| Index: appengine/components/components/config/endpoint.py
 | 
| diff --git a/appengine/components/components/config/endpoint.py b/appengine/components/components/config/endpoint.py
 | 
| index e7c9e4ebd31b0cb7f941c0b8c8716993b10c5df8..c48434ef1280cad892992f624a2fe8d9a35c0ba5 100644
 | 
| --- a/appengine/components/components/config/endpoint.py
 | 
| +++ b/appengine/components/components/config/endpoint.py
 | 
| @@ -5,24 +5,36 @@
 | 
|  """Cloud Endpoints API for configs.
 | 
|  
 | 
|  * Reads/writes config service location.
 | 
| -* Validates configs. TODO(nodir): implement.
 | 
| +* Validates configs.
 | 
| +* Provides service metadata.
 | 
|  """
 | 
|  
 | 
|  import logging
 | 
|  
 | 
| -from components import auth
 | 
|  from protorpc import messages
 | 
|  from protorpc import message_types
 | 
|  from protorpc import remote
 | 
| +import endpoints
 | 
| +
 | 
| +from components import auth
 | 
|  
 | 
|  from . import common
 | 
|  from . import validation
 | 
|  
 | 
|  
 | 
| +METADATA_FORMAT_VERSION = "1.0"
 | 
| +
 | 
| +
 | 
| +def get_default_rule_set():
 | 
| +  return validation.DEFAULT_RULE_SET
 | 
| +
 | 
| +
 | 
|  class ConfigSettingsMessage(messages.Message):
 | 
| -  """Configuration service location."""
 | 
| +  """Configuration service location. Resembles common.ConfigSettings"""
 | 
|    # Example: 'luci-config.appspot.com'
 | 
|    service_hostname = messages.StringField(1)
 | 
| +  # Example: 'user:luci-config@appspot.gserviceaccount.com'
 | 
| +  trusted_config_account = messages.StringField(2)
 | 
|  
 | 
|  
 | 
|  class ValidateRequestMessage(messages.Message):
 | 
| @@ -46,6 +58,50 @@ class ValidateResponseMessage(messages.Message):
 | 
|    messages = messages.MessageField(ValidationMessage, 1, repeated=True)
 | 
|  
 | 
|  
 | 
| +def is_trusted_requester():
 | 
| +  """Returns True if the requester can see the service metadata.
 | 
| +
 | 
| +  Used in metadata endpoint.
 | 
| +
 | 
| +  Returns:
 | 
| +    True if the current identity is an admin or the config service.
 | 
| +  """
 | 
| +  if auth.is_admin():
 | 
| +    return True
 | 
| +
 | 
| +  settings = common.ConfigSettings.cached()
 | 
| +  if settings and settings.trusted_config_account:
 | 
| +    identity = auth.get_current_identity()
 | 
| +    if identity == settings.trusted_config_account:
 | 
| +      return True
 | 
| +
 | 
| +  return False
 | 
| +
 | 
| +
 | 
| +class ConfigPattern(messages.Message):
 | 
| +  """A pattern for one config file. See ServiceDynamicMetadata."""
 | 
| +  config_set = messages.StringField(1, required=True)
 | 
| +  path = messages.StringField(2, required=True)
 | 
| +
 | 
| +
 | 
| +class ServiceDynamicMetadata(messages.Message):
 | 
| +  """Equivalent of config_service's ServiceDynamicMetadata proto message.
 | 
| +
 | 
| +  Keep this class in sync with:
 | 
| +    * ServiceDynamicMetadata message in
 | 
| +      appengine/config_service/proto/service_config.proto
 | 
| +    * validation.validate_service_dynamic_metadata_blob()
 | 
| +    * services._dict_to_dynamic_metadata()
 | 
| +  """
 | 
| +
 | 
| +  class Validator(messages.Message):
 | 
| +    patterns = messages.MessageField(ConfigPattern, 1, repeated=True)
 | 
| +    url = messages.StringField(2, required=True)
 | 
| +
 | 
| +  version = messages.StringField(1, required=True)
 | 
| +  validation = messages.MessageField(Validator, 2)
 | 
| +
 | 
| +
 | 
|  @auth.endpoints_api(name='config', version='v1', title='Configuration service')
 | 
|  class ConfigApi(remote.Service):
 | 
|    """Configuration service."""
 | 
| @@ -57,14 +113,27 @@ class ConfigApi(remote.Service):
 | 
|    def settings(self, request):
 | 
|      """Reads/writes config service location. Accessible only by admins."""
 | 
|      settings = common.ConfigSettings.fetch() or common.ConfigSettings()
 | 
| +    delta = {}
 | 
|      if request.service_hostname is not None:
 | 
| -      # Change only if service_hostname was specified.
 | 
| -      changed = settings.modify(service_hostname=request.service_hostname)
 | 
| -      if changed:
 | 
| -        logging.warning('Updated config settings')
 | 
| +      delta['service_hostname'] = request.service_hostname
 | 
| +    if request.trusted_config_account is not None:
 | 
| +      try:
 | 
| +        delta['trusted_config_account'] = auth.Identity.from_bytes(
 | 
| +            request.trusted_config_account)
 | 
| +      except ValueError as ex:
 | 
| +        raise endpoints.BadRequestException(
 | 
| +            'Invalid trusted_config_account %s: %s' % (
 | 
| +              request.trusted_config_account,
 | 
| +              ex.message))
 | 
| +    changed = settings.modify(**delta)
 | 
| +    if changed:
 | 
| +      logging.warning('Updated config settings')
 | 
|      settings = common.ConfigSettings.fetch() or settings
 | 
|      return ConfigSettingsMessage(
 | 
|          service_hostname=settings.service_hostname,
 | 
| +        trusted_config_account=(
 | 
| +            settings.trusted_config_account.to_bytes()
 | 
| +            if settings.trusted_config_account else None)
 | 
|      )
 | 
|  
 | 
|    @auth.endpoints_method(
 | 
| @@ -85,3 +154,24 @@ class ConfigApi(remote.Service):
 | 
|            text=m.text,
 | 
|        ))
 | 
|      return res
 | 
| +
 | 
| +  @auth.endpoints_method(
 | 
| +      message_types.VoidMessage, ServiceDynamicMetadata, path='metadata')
 | 
| +  @auth.require(is_trusted_requester)
 | 
| +  def get_metadata(self, _request):
 | 
| +    """Describes a service. Used by config service to discover other services.
 | 
| +    """
 | 
| +    meta = ServiceDynamicMetadata(version=METADATA_FORMAT_VERSION)
 | 
| +    http_headers = dict(self.request_state.headers)
 | 
| +    assert 'host' in http_headers, http_headers
 | 
| +    meta.validation = meta.Validator(
 | 
| +        url='https://{hostname}/_ah/api/{name}/{version}/{path}validate'.format(
 | 
| +            hostname=http_headers['host'],
 | 
| +            name=self.api_info.name,
 | 
| +            version=self.api_info.version,
 | 
| +            path=self.api_info.path or '',
 | 
| +        )
 | 
| +    )
 | 
| +    for p in sorted(get_default_rule_set().patterns()):
 | 
| +      meta.validation.patterns.append(ConfigPattern(**p._asdict()))
 | 
| +    return meta
 | 
| 
 |