blob: d5d6a25d892fa9da4454fce757514cadcbbc7334 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import base64
11import json
12import logging
13import os
14import time
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020015from six.moves import urllib
Copybara854996b2021-09-07 19:36:02 +000016import webapp2
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020017import flask
Copybara854996b2021-09-07 19:36:02 +000018
19from google.appengine.api import app_identity
20from google.appengine.api import urlfetch
21from google.appengine.ext import db
22from google.protobuf import text_format
23
24from infra_libs import ts_mon
25
26import settings
27from framework import framework_constants
28from proto import api_clients_config_pb2
29
30
31CONFIG_FILE_PATH = os.path.join(
32 os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
33 'testing', 'api_clients.cfg')
34LUCI_CONFIG_URL = (
35 'https://luci-config.appspot.com/_ah/api/config/v1/config_sets'
36 '/services/monorail-prod/config/api_clients.cfg')
37
38
39client_config_svc = None
40service_account_map = None
41qpm_dict = None
42allowed_origins_set = None
43
44
45class ClientConfig(db.Model):
46 configs = db.TextProperty()
47
48
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020049_CONFIG_LOADS = ts_mon.CounterMetric(
50 'monorail/client_config_svc/loads', 'Results of fetches from luci-config.',
51 [ts_mon.BooleanField('success'),
52 ts_mon.StringField('type')])
Copybara854996b2021-09-07 19:36:02 +000053
Copybara854996b2021-09-07 19:36:02 +000054
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020055def _process_response(response):
56 try:
57 content = json.loads(response.content)
58 except ValueError:
59 logging.error('Response was not JSON: %r', response.content)
60 _CONFIG_LOADS.increment({'success': False, 'type': 'json-load-error'})
61 raise
62
63 try:
64 config_content = content['content']
65 except KeyError:
66 logging.error('JSON contained no content: %r', content)
67 _CONFIG_LOADS.increment({'success': False, 'type': 'json-key-error'})
68 raise
69
70 try:
71 content_text = base64.b64decode(config_content)
72 except TypeError:
73 logging.error('Content was not b64: %r', config_content)
74 _CONFIG_LOADS.increment({'success': False, 'type': 'b64-decode-error'})
75 raise
76
77 try:
78 cfg = api_clients_config_pb2.ClientCfg()
79 text_format.Merge(content_text, cfg)
80 except:
81 logging.error('Content was not a valid ClientCfg proto: %r', content_text)
82 _CONFIG_LOADS.increment({'success': False, 'type': 'proto-load-error'})
83 raise
84
85 return content_text
86
87
88def GetLoadApiClientConfigs():
89 global service_account_map
90 global qpm_dict
91 authorization_token, _ = app_identity.get_access_token(
Copybara854996b2021-09-07 19:36:02 +000092 framework_constants.OAUTH_SCOPE)
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020093 response = urlfetch.fetch(
Copybara854996b2021-09-07 19:36:02 +000094 LUCI_CONFIG_URL,
95 method=urlfetch.GET,
96 follow_redirects=False,
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020097 headers={
98 'Content-Type': 'application/json; charset=UTF-8',
99 'Authorization': 'Bearer ' + authorization_token
100 })
Copybara854996b2021-09-07 19:36:02 +0000101
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +0200102 if response.status_code != 200:
103 logging.error('Invalid response from luci-config: %r', response)
104 _CONFIG_LOADS.increment({'success': False, 'type': 'luci-cfg-error'})
105 flask.abort(500, 'Invalid response from luci-config')
Copybara854996b2021-09-07 19:36:02 +0000106
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +0200107 try:
108 content_text = _process_response(response)
109 except Exception as e:
110 flask.abort(500, str(e))
Copybara854996b2021-09-07 19:36:02 +0000111
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +0200112 logging.info('luci-config content decoded: %r.', content_text)
113 configs = ClientConfig(configs=content_text, key_name='api_client_configs')
114 configs.put()
115 service_account_map = None
116 qpm_dict = None
117 _CONFIG_LOADS.increment({'success': True, 'type': 'success'})
Copybara854996b2021-09-07 19:36:02 +0000118
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +0200119 return ''
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200120
Copybara854996b2021-09-07 19:36:02 +0000121
122class ClientConfigService(object):
123 """The persistence layer for client config data."""
124
125 # Reload no more than once every 15 minutes.
126 # Different GAE instances can load it at different times,
127 # so clients may get inconsistence responses shortly after allowlisting.
128 EXPIRES_IN = 15 * framework_constants.SECS_PER_MINUTE
129
130 def __init__(self):
131 self.client_configs = None
132 self.load_time = 0
133
134 def GetConfigs(self, use_cache=True, cur_time=None):
135 """Read client configs."""
136
137 cur_time = cur_time or int(time.time())
138 force_load = False
139 if not self.client_configs:
140 force_load = True
141 elif not use_cache:
142 force_load = True
143 elif cur_time - self.load_time > self.EXPIRES_IN:
144 force_load = True
145
146 if force_load:
147 if settings.local_mode or settings.unit_test_mode:
148 self._ReadFromFilesystem()
149 else:
150 self._ReadFromDatastore()
151
152 return self.client_configs
153
154 def _ReadFromFilesystem(self):
155 try:
156 with open(CONFIG_FILE_PATH, 'r') as f:
157 content_text = f.read()
158 logging.info('Read client configs from local file.')
159 cfg = api_clients_config_pb2.ClientCfg()
160 text_format.Merge(content_text, cfg)
161 self.client_configs = cfg
162 self.load_time = int(time.time())
163 except Exception as e:
164 logging.exception('Failed to read client configs: %s', e)
165
166 def _ReadFromDatastore(self):
167 entity = ClientConfig.get_by_key_name('api_client_configs')
168 if entity:
169 cfg = api_clients_config_pb2.ClientCfg()
170 text_format.Merge(entity.configs, cfg)
171 self.client_configs = cfg
172 self.load_time = int(time.time())
173 else:
174 logging.error('Failed to get api client configs from datastore.')
175
176 def GetClientIDEmails(self):
177 """Get client IDs and Emails."""
178 self.GetConfigs(use_cache=True)
179 client_ids = [c.client_id for c in self.client_configs.clients]
180 client_emails = [c.client_email for c in self.client_configs.clients]
181 return client_ids, client_emails
182
183 def GetDisplayNames(self):
184 """Get client display names."""
185 self.GetConfigs(use_cache=True)
186 names_dict = {}
187 for client in self.client_configs.clients:
188 if client.display_name:
189 names_dict[client.client_email] = client.display_name
190 return names_dict
191
192 def GetQPM(self):
193 """Get client qpm limit."""
194 self.GetConfigs(use_cache=True)
195 qpm_map = {}
196 for client in self.client_configs.clients:
197 if client.HasField('qpm_limit'):
198 qpm_map[client.client_email] = client.qpm_limit
199 return qpm_map
200
201 def GetAllowedOriginsSet(self):
202 """Get the set of all allowed origins."""
203 self.GetConfigs(use_cache=True)
204 origins = set()
205 for client in self.client_configs.clients:
206 origins.update(client.allowed_origins)
207 return origins
208
209
210def GetClientConfigSvc():
211 global client_config_svc
212 if client_config_svc is None:
213 client_config_svc = ClientConfigService()
214 return client_config_svc
215
216
217def GetServiceAccountMap():
218 # typ: () -> Mapping[str, str]
219 """Returns only service accounts that have specified display_names."""
220 global service_account_map
221 if service_account_map is None:
222 service_account_map = GetClientConfigSvc().GetDisplayNames()
223 return service_account_map
224
225
226def GetQPMDict():
227 global qpm_dict
228 if qpm_dict is None:
229 qpm_dict = GetClientConfigSvc().GetQPM()
230 return qpm_dict
231
232
233def GetAllowedOriginsSet():
234 global allowed_origins_set
235 if allowed_origins_set is None:
236 allowed_origins_set = GetClientConfigSvc().GetAllowedOriginsSet()
237 return allowed_origins_set