Index: third_party/google-endpoints/google/api/control/service.py |
diff --git a/third_party/google-endpoints/google/api/control/service.py b/third_party/google-endpoints/google/api/control/service.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..96304f9ff70055c5fcfed1d1c7b2fb4ae87d9461 |
--- /dev/null |
+++ b/third_party/google-endpoints/google/api/control/service.py |
@@ -0,0 +1,507 @@ |
+# 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. |
+ |
+"""service provides funcs for working with ``Service`` instances. |
+ |
+:func:`extract_report_spec` obtains objects used to determine what metrics, |
+labels and logs are included in a report request. |
+ |
+:class:`MethodRegistry` obtains a registry of `MethodInfo` instances from the |
+data within a `Service` which can then be used to determine which methods get |
+tracked. |
+ |
+:class:`Loaders` enumerates the different ways in which to obtain a usable |
+``Service`` instance |
+ |
+""" |
+ |
+from __future__ import absolute_import |
+ |
+import collections |
+import logging |
+import os |
+ |
+ |
+from apitools.base.py import encoding |
+from enum import Enum |
+ |
+from . import label_descriptor, metric_descriptor, messages, path_template |
+from google.api.config import service_config |
+ |
+ |
+logger = logging.getLogger(__name__) |
+ |
+ |
+CONFIG_VAR = 'ENDPOINTS_SERVICE_CONFIG_FILE' |
+ |
+ |
+def _load_from_well_known_env(): |
+ if CONFIG_VAR not in os.environ: |
+ logger.info('did not load service; no environ var %s', CONFIG_VAR) |
+ return None |
+ config_file = os.environ[CONFIG_VAR] |
+ if not os.path.exists(os.environ[CONFIG_VAR]): |
+ logger.warn('did not load service; missing config file %s', config_file) |
+ return None |
+ try: |
+ with open(config_file) as f: |
+ return encoding.JsonToMessage(messages.Service, f.read()) |
+ except ValueError: |
+ logger.warn('did not load service; bad json config file %s', config_file) |
+ return None |
+ |
+ |
+_SIMPLE_CONFIG = """ |
+{ |
+ "name": "allow-all", |
+ "http": { |
+ "rules": [{ |
+ "selector": "allow-all.GET", |
+ "get": "**" |
+ }, { |
+ "selector": "allow-all.POST", |
+ "post": "**" |
+ }] |
+ }, |
+ "usage": { |
+ "rules": [{ |
+ "selector" : "allow-all.GET", |
+ "allowUnregisteredCalls" : true |
+ }, { |
+ "selector" : "allow-all.POST", |
+ "allowUnregisteredCalls" : true |
+ }] |
+ } |
+} |
+""" |
+_SIMPLE_CORE = encoding.JsonToMessage(messages.Service, _SIMPLE_CONFIG) |
+ |
+ |
+def _load_simple(): |
+ return encoding.CopyProtoMessage(_SIMPLE_CORE) |
+ |
+ |
+class Loaders(Enum): |
+ """Enumerates the functions used to load service configs.""" |
+ # pylint: disable=too-few-public-methods |
+ ENVIRONMENT = (_load_from_well_known_env,) |
+ SIMPLE = (_load_simple,) |
+ FROM_SERVICE_MANAGEMENT = (service_config.fetch_service_config,) |
+ |
+ def __init__(self, load_func): |
+ """Constructor. |
+ |
+ load_func is used to load a service config |
+ """ |
+ self._load_func = load_func |
+ |
+ def load(self, **kw): |
+ return self._load_func(**kw) |
+ |
+ |
+class MethodRegistry(object): |
+ """Provides a registry of the api methods defined by a ``Service``. |
+ |
+ During construction, ``MethodInfo`` instances are extracted from a |
+ ``Service``. The are subsequently accessible via the :func:`lookup` method. |
+ |
+ """ |
+ # pylint: disable=too-few-public-methods |
+ _OPTIONS = 'OPTIONS' |
+ |
+ def __init__(self, service): |
+ """Constructor. |
+ |
+ Args: |
+ service (:class:`google.api.gen.servicecontrol_v1_messages.Service`): |
+ a service instance |
+ """ |
+ if not isinstance(service, messages.Service): |
+ raise ValueError('service should be an instance of Service') |
+ if not service.name: |
+ raise ValueError('Bad service: the name is missing') |
+ |
+ self._service = service # the service that provides the methods |
+ self._extracted_methods = {} # tracks all extracted_methods by selector |
+ |
+ self._auth_infos = self._extract_auth_config() |
+ |
+ # tracks urls templates |
+ self._templates_method_infos = collections.defaultdict(list) |
+ self._extract_methods() |
+ |
+ def lookup(self, http_method, path): |
+ http_method = http_method.lower() |
+ if path.startswith('/'): |
+ path = path[1:] |
+ tmi = self._templates_method_infos.get(http_method) |
+ if not tmi: |
+ logger.debug('No methods for http method %s in %s', |
+ http_method, |
+ self._templates_method_infos.keys()) |
+ return None |
+ |
+ # pylint: disable=fixme |
+ # TODO: speed this up if it proves to be bottleneck. |
+ # |
+ # There is sophisticated trie-based solution in esp, something similar |
+ # could be built around the path_template implementation |
+ for template, method_info in tmi: |
+ logger.debug('trying %s with template %s', path, template) |
+ try: |
+ template.match(path) |
+ logger.debug('%s matched template %s', path, template) |
+ return method_info |
+ except path_template.ValidationException: |
+ logger.debug('%s did not match template %s', path, template) |
+ continue |
+ |
+ return None |
+ |
+ def _extract_auth_config(self): |
+ """Obtains the authentication configurations.""" |
+ |
+ service = self._service |
+ if not service.authentication: |
+ return {} |
+ |
+ auth_infos = {} |
+ for auth_rule in service.authentication.rules: |
+ selector = auth_rule.selector |
+ provider_ids_to_audiences = {} |
+ for requirement in auth_rule.requirements: |
+ provider_id = requirement.providerId |
+ if provider_id and requirement.audiences: |
+ audiences = requirement.audiences.split(",") |
+ provider_ids_to_audiences[provider_id] = audiences |
+ auth_infos[selector] = AuthInfo(provider_ids_to_audiences) |
+ return auth_infos |
+ |
+ def _extract_methods(self): |
+ """Obtains the methods used in the service.""" |
+ service = self._service |
+ all_urls = set() |
+ urls_with_options = set() |
+ if not service.http: |
+ return |
+ for rule in service.http.rules: |
+ http_method, url = _detect_pattern_option(rule) |
+ if not url or not http_method or not rule.selector: |
+ logger.error('invalid HTTP binding encountered') |
+ continue |
+ |
+ # Obtain the method info |
+ method_info = self._get_or_create_method_info(rule.selector) |
+ if rule.body: |
+ method_info.body_field_path = rule.body |
+ if not self._register(http_method, url, method_info): |
+ continue # detected an invalid url |
+ all_urls.add(url) |
+ |
+ if http_method == self._OPTIONS: |
+ urls_with_options.add(url) |
+ |
+ self._add_cors_options_selectors(all_urls - urls_with_options) |
+ self._update_usage() |
+ self._update_system_parameters() |
+ |
+ def _register(self, http_method, url, method_info): |
+ try: |
+ http_method = http_method.lower() |
+ template = path_template.PathTemplate(url) |
+ self._templates_method_infos[http_method].append((template, method_info)) |
+ logger.debug('Registered template %s under method %s', |
+ template, |
+ http_method) |
+ return True |
+ except path_template.ValidationException: |
+ logger.error('invalid HTTP template provided: %s', url) |
+ return False |
+ |
+ def _update_usage(self): |
+ extracted_methods = self._extracted_methods |
+ service = self._service |
+ if not service.usage: |
+ return |
+ for rule in service.usage.rules: |
+ selector = rule.selector |
+ method = extracted_methods.get(selector) |
+ if method: |
+ method.allow_unregistered_calls = rule.allowUnregisteredCalls |
+ else: |
+ logger.error('bad usage selector: No HTTP rule for %s', selector) |
+ |
+ def _get_or_create_method_info(self, selector): |
+ extracted_methods = self._extracted_methods |
+ info = self._extracted_methods.get(selector) |
+ if info: |
+ return info |
+ |
+ auth_infos = self._auth_infos |
+ auth_info = auth_infos[selector] if selector in auth_infos else None |
+ |
+ info = MethodInfo(selector, auth_info) |
+ extracted_methods[selector] = info |
+ return info |
+ |
+ def _add_cors_options_selectors(self, urls): |
+ extracted_methods = self._extracted_methods |
+ base_selector = '%s.%s' % (self._service.name, self._OPTIONS) |
+ |
+ # ensure that no existing options selector is being used |
+ options_selector = base_selector |
+ n = 0 |
+ while extracted_methods.get(options_selector) is not None: |
+ n += 1 |
+ options_selector = '%s.%d' % (base_selector, n) |
+ method_info = self._get_or_create_method_info(options_selector) |
+ method_info.allow_unregistered_calls = True |
+ for u in urls: |
+ self._register(self._OPTIONS, u, method_info) |
+ |
+ def _update_system_parameters(self): |
+ extracted_methods = self._extracted_methods |
+ service = self._service |
+ if not service.systemParameters: |
+ return |
+ rules = service.systemParameters.rules |
+ for rule in rules: |
+ selector = rule.selector |
+ method = extracted_methods.get(selector) |
+ if not method: |
+ logger.error('bad system parameter: No HTTP rule for %s', |
+ selector) |
+ continue |
+ |
+ for parameter in rule.parameters: |
+ name = parameter.name |
+ if not name: |
+ logger.error('bad system parameter: no parameter name %s', |
+ selector) |
+ continue |
+ |
+ if parameter.httpHeader: |
+ method.add_header_param(name, parameter.httpHeader) |
+ if parameter.urlQueryParameter: |
+ method.add_url_query_param(name, parameter.urlQueryParameter) |
+ |
+ |
+class AuthInfo(object): |
+ """Consolidates auth information about methods defined in a ``Service``.""" |
+ |
+ def __init__(self, provider_ids_to_audiences): |
+ """Construct an AuthInfo instance. |
+ |
+ Args: |
+ provider_ids_to_audiences: a dictionary that maps from provider ids |
+ to allowed audiences. |
+ """ |
+ self._provider_ids_to_audiences = provider_ids_to_audiences |
+ |
+ def is_provider_allowed(self, provider_id): |
+ return provider_id in self._provider_ids_to_audiences |
+ |
+ def get_allowed_audiences(self, provider_id): |
+ return self._provider_ids_to_audiences.get(provider_id, []) |
+ |
+ |
+class MethodInfo(object): |
+ """Consolidates information about methods defined in a ``Service``.""" |
+ API_KEY_NAME = 'api_key' |
+ # pylint: disable=too-many-instance-attributes |
+ |
+ def __init__(self, selector, auth_info): |
+ self.selector = selector |
+ self.auth_info = auth_info |
+ self.allow_unregistered_calls = False |
+ self.backend_address = '' |
+ self.body_field_path = '' |
+ self._url_query_parameters = collections.defaultdict(list) |
+ self._header_parameters = collections.defaultdict(list) |
+ |
+ def add_url_query_param(self, name, parameter): |
+ self._url_query_parameters[name].append(parameter) |
+ |
+ def add_header_param(self, name, parameter): |
+ self._header_parameters[name].append(parameter) |
+ |
+ def url_query_param(self, name): |
+ return tuple(self._url_query_parameters[name]) |
+ |
+ def header_param(self, name): |
+ return tuple(self._header_parameters[name]) |
+ |
+ @property |
+ def api_key_http_header(self): |
+ return self.header_param(self.API_KEY_NAME) |
+ |
+ @property |
+ def api_key_url_query_params(self): |
+ return self.url_query_param(self.API_KEY_NAME) |
+ |
+ |
+def extract_report_spec( |
+ service, |
+ label_is_supported=label_descriptor.KnownLabels.is_supported, |
+ metric_is_supported=metric_descriptor.KnownMetrics.is_supported): |
+ """Obtains the used logs, metrics and labels from a service. |
+ |
+ label_is_supported and metric_is_supported are filter functions used to |
+ determine if label_descriptors or metric_descriptors found in the service |
+ are supported. |
+ |
+ Args: |
+ service (:class:`google.api.gen.servicecontrol_v1_messages.Service`): |
+ a service instance |
+ label_is_supported (:func): determines if a given label is supported |
+ metric_is_supported (:func): determines if a given metric is supported |
+ |
+ Return: |
+ tuple: ( |
+ logs (set[string}), # the logs to report to |
+ metrics (list[string]), # the metrics to use |
+ labels (list[string]) # the labels to add |
+ ) |
+ """ |
+ resource_descs = service.monitoredResources |
+ labels_dict = {} |
+ logs = set() |
+ if service.logging: |
+ logs = _add_logging_destinations( |
+ service.logging.producerDestinations, |
+ resource_descs, |
+ service.logs, |
+ labels_dict, |
+ label_is_supported |
+ ) |
+ metrics_dict = {} |
+ monitoring = service.monitoring |
+ if monitoring: |
+ for destinations in (monitoring.consumerDestinations, |
+ monitoring.producerDestinations): |
+ _add_monitoring_destinations(destinations, |
+ resource_descs, |
+ service.metrics, |
+ metrics_dict, |
+ metric_is_supported, |
+ labels_dict, |
+ label_is_supported) |
+ return logs, metrics_dict.keys(), labels_dict.keys() |
+ |
+ |
+def _add_logging_destinations(destinations, |
+ resource_descs, |
+ log_descs, |
+ labels_dict, |
+ is_supported): |
+ all_logs = set() |
+ for d in destinations: |
+ if not _add_labels_for_a_monitored_resource(resource_descs, |
+ d.monitoredResource, |
+ labels_dict, |
+ is_supported): |
+ continue # skip bad monitored resources |
+ for log in d.logs: |
+ if _add_labels_for_a_log(log_descs, log, labels_dict, is_supported): |
+ all_logs.add(log) # only add correctly configured logs |
+ return all_logs |
+ |
+ |
+def _add_monitoring_destinations(destinations, |
+ resource_descs, |
+ metric_descs, |
+ metrics_dict, |
+ metric_is_supported, |
+ labels_dict, |
+ label_is_supported): |
+ # pylint: disable=too-many-arguments |
+ for d in destinations: |
+ if not _add_labels_for_a_monitored_resource(resource_descs, |
+ d.monitoredResource, |
+ labels_dict, |
+ label_is_supported): |
+ continue # skip bad monitored resources |
+ for metric_name in d.metrics: |
+ metric_desc = _find_metric_descriptor(metric_descs, metric_name, |
+ metric_is_supported) |
+ if not metric_desc: |
+ continue # skip unrecognized or unsupported metric |
+ if not _add_labels_from_descriptors(metric_desc.labels, labels_dict, |
+ label_is_supported): |
+ continue # skip metrics with bad labels |
+ metrics_dict[metric_name] = metric_desc |
+ |
+ |
+def _add_labels_from_descriptors(descs, labels_dict, is_supported): |
+ # only add labels if there are no conflicts |
+ for desc in descs: |
+ existing = labels_dict.get(desc.key) |
+ if existing and existing.valueType != desc.valueType: |
+ logger.warn('halted label scan: conflicting label in %s', desc.key) |
+ return False |
+ # Update labels_dict |
+ for desc in descs: |
+ if is_supported(desc): |
+ labels_dict[desc.key] = desc |
+ return True |
+ |
+ |
+def _add_labels_for_a_log(logging_descs, log_name, labels_dict, is_supported): |
+ for d in logging_descs: |
+ if d.name == log_name: |
+ _add_labels_from_descriptors(d.labels, labels_dict, is_supported) |
+ return True |
+ logger.warn('bad log label scan: log not found %s', log_name) |
+ return False |
+ |
+ |
+def _add_labels_for_a_monitored_resource(resource_descs, |
+ resource_name, |
+ labels_dict, |
+ is_supported): |
+ for d in resource_descs: |
+ if d.type == resource_name: |
+ _add_labels_from_descriptors(d.labels, labels_dict, is_supported) |
+ return True |
+ logger.warn('bad monitored resource label scan: resource not found %s', |
+ resource_name) |
+ return False |
+ |
+ |
+def _find_metric_descriptor(metric_descs, name, metric_is_supported): |
+ for d in metric_descs: |
+ if name != d.name: |
+ continue |
+ if metric_is_supported(d): |
+ return d |
+ else: |
+ return None |
+ return None |
+ |
+ |
+# This is derived from the oneof choices of the HttpRule message's pattern |
+# field in google/api/http.proto, and should be kept in sync with that |
+_HTTP_RULE_ONE_OF_FIELDS = ( |
+ 'get', 'put', 'post', 'delete', 'patch', 'custom') |
+ |
+ |
+def _detect_pattern_option(http_rule): |
+ for f in _HTTP_RULE_ONE_OF_FIELDS: |
+ value = http_rule.get_assigned_value(f) |
+ if value is not None: |
+ if f == 'custom': |
+ return value.kind, value.path |
+ else: |
+ return f, value |
+ return None, None |