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

Side by Side Diff: appengine/config_service/validation_test.py

Issue 1224913002: luci-config: fine-grained acls (Closed) Base URL: git@github.com:luci/luci-py.git@master
Patch Set: identities in configs Created 5 years, 5 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 unified diff | Download patch
« no previous file with comments | « appengine/config_service/validation.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2015 The Swarming Authors. All rights reserved. 2 # Copyright 2015 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed by the Apache v2.0 license that can be 3 # Use of this source code is governed by the Apache v2.0 license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 import base64 6 import base64
7 import logging 7 import logging
8 8
9 from test_env import future 9 from test_env import future
10 import test_env 10 import test_env
11 test_env.setup_test_env() 11 test_env.setup_test_env()
12 12
13 from google.appengine.ext import ndb 13 from google.appengine.ext import ndb
14 14
15 from test_support import test_case 15 from test_support import test_case
16 import mock 16 import mock
17 17
18 from components import config 18 from components import config
19 from components import net 19 from components import net
20 from components.config import validation_context 20 from components.config import validation_context
21 21
22 from proto import project_config_pb2 22 from proto import project_config_pb2
23 from proto import service_config_pb2 23 from proto import service_config_pb2
24 import services
24 import storage 25 import storage
25 import validation 26 import validation
26 27
27 28
28 class ValidationTestCase(test_case.TestCase): 29 class ValidationTestCase(test_case.TestCase):
29 def test_validate_validation_cfg(self): 30 def setUp(self):
30 cfg = ''' 31 super(ValidationTestCase, self).setUp()
31 rules { 32 self.services = []
32 config_set: "projects/foo" 33 self.mock(services, 'get_services_async', lambda: future(self.services))
33 path: "bar.cfg"
34 url: "https://foo.com/validate_config"
35 }
36 rules {
37 config_set: "regex:projects\/foo"
38 path: "regex:.+"
39 url: "https://foo.com/validate_config"
40 }
41 rules {
42 config_set: "bad config set name"
43 path: "regex:))bad regex"
44 # no url
45 }
46 rules {
47 config_set: "regex:)("
48 path: "/bar.cfg"
49 url: "http://not-https.com"
50 }
51 rules {
52 config_set: "projects/foo"
53 path: "a/../b.cfg"
54 url: "https://foo.com/validate_config"
55 }
56 rules {
57 config_set: "projects/foo"
58 path: "a/./b.cfg"
59 url: "/no/hostname"
60 }
61 '''
62 result = validation.validate_config(
63 config.self_config_set(), 'validation.cfg', cfg)
64
65 self.assertEqual(
66 [m.text for m in result.messages],
67 [
68 'Rule #3: config_set: invalid config set: bad config set name',
69 ('Rule #3: path: invalid regular expression "))bad regex": '
70 'unbalanced parenthesis'),
71 'Rule #3: url: not specified',
72 ('Rule #4: config_set: invalid regular expression ")(": '
73 'unbalanced parenthesis'),
74 'Rule #4: path: must not be absolute: /bar.cfg',
75 'Rule #4: url: scheme must be "https"',
76 'Rule #5: path: must not contain ".." or "." components: a/../b.cfg',
77 'Rule #6: path: must not contain ".." or "." components: a/./b.cfg',
78 'Rule #6: url: hostname not specified',
79 'Rule #6: url: scheme must be "https"',
80 ]
81 )
82 34
83 def test_validate_project_registry(self): 35 def test_validate_project_registry(self):
84 cfg = ''' 36 cfg = '''
85 projects { 37 projects {
86 id: "a" 38 id: "a"
87 config_location { 39 config_location {
88 storage_type: GITILES 40 storage_type: GITILES
89 url: "https://a.googlesource.com/project" 41 url: "https://a.googlesource.com/project"
90 } 42 }
91 } 43 }
(...skipping 20 matching lines...) Expand all
112 self.assertEqual( 64 self.assertEqual(
113 [m.text for m in result.messages], 65 [m.text for m in result.messages],
114 [ 66 [
115 'Project b: config_location: storage_type is not set', 67 'Project b: config_location: storage_type is not set',
116 'Project a: id is not unique', 68 'Project a: id is not unique',
117 ('Project a: config_location: Invalid Gitiles repo url: ' 69 ('Project a: config_location: Invalid Gitiles repo url: '
118 'https://no-project.googlesource.com'), 70 'https://no-project.googlesource.com'),
119 'Project #4: id is not specified', 71 'Project #4: id is not specified',
120 ('Project #4: config_location: Invalid Gitiles repo url: ' 72 ('Project #4: config_location: Invalid Gitiles repo url: '
121 'https://no-project.googlesource.com/bad_plus/+'), 73 'https://no-project.googlesource.com/bad_plus/+'),
122 'Project list is not sorted by id. First offending id: a', 74 'Projects are not sorted by id. First offending id: a',
123 ] 75 ]
124 ) 76 )
125 77
78 def test_validate_services_registry(self):
79 cfg = '''
80 services {
81 id: "a"
82 access: "a@a.com"
83 access: "user:a@a.com"
84 access: "group:abc"
85 }
86 services {
87 owners: "not an email"
88 config_location {
89 storage_type: GITILES
90 url: "../some"
91 }
92 metadata_url: "not an url"
93 access: "**&"
94 access: "group:**&"
95 access: "a:b"
96 }
97 services {
98 id: "b"
99 config_location {
100 storage_type: GITILES
101 url: "https://gitiles.host.com/project"
102 }
103 }
104 services {
105 id: "a-unsorted"
106 }
107 '''
108 result = validation.validate_config(
109 config.self_config_set(), 'services.cfg', cfg)
110
111 self.assertEqual(
112 [m.text for m in result.messages],
113 [
114 'Service #2: id is not specified',
115 ('Service #2: config_location: '
116 'storage_type must not be set if relative url is used'),
117 'Service #2: invalid email: "not an email"',
118 'Service #2: metadata_url: hostname not specified',
119 'Service #2: metadata_url: scheme must be "https"',
120 'Service #2: access #1: invalid email: "**&"',
121 'Service #2: access #2: invalid group: **&',
122 'Service #2: access #3: Identity has invalid format: b',
123 'Services are not sorted by id. First offending id: a-unsorted',
124 ]
125 )
126
127
128 def test_validate_service_dynamic_metadata_blob(self):
129 def expect_errors(blob, expected_messages):
130 ctx = config.validation.Context()
131 validation.validate_service_dynamic_metadata_blob(blob, ctx)
132 self.assertEqual(
133 [m.text for m in ctx.result().messages], expected_messages)
134
135 expect_errors([], ['Service dynamic metadata must be an object'])
136 expect_errors({}, [])
137 expect_errors({'validation': 'bad'}, ['validation: must be an object'])
138 expect_errors(
139 {
140 'validation': {
141 'patterns': 'bad',
142 }
143 },
144 [
145 'validation: url: not specified',
146 'validation: patterns must be a list',
147 ])
148 expect_errors(
149 {
150 'validation': {
151 'url': 'bad url',
152 'patterns': [
153 'bad',
154 {
155 },
156 {
157 'config_set': 'a:b',
158 'path': '/foo',
159 },
160 {
161 'config_set': 'regex:)(',
162 'path': '../b',
163 },
164 {
165 'config_set': 'projects/foo',
166 'path': 'bar.cfg',
167 },
168 ]
169 }
170 },
171 [
172 'validation: url: hostname not specified',
173 'validation: url: scheme must be "https"',
174 'validation: pattern #1: must be an object',
175 'validation: pattern #2: config_set: Pattern must be a string',
176 'validation: pattern #2: path: Pattern must be a string',
177 'validation: pattern #3: config_set: Invalid pattern kind: a',
178 'validation: pattern #3: path: must not be absolute: /foo',
179 'validation: pattern #4: config_set: unbalanced parenthesis',
180 ('validation: pattern #4: path: '
181 'must not contain ".." or "." components: ../b'),
182 ]
183 )
184
126 def test_validate_schemas(self): 185 def test_validate_schemas(self):
127 cfg = ''' 186 cfg = '''
128 schemas { 187 schemas {
129 name: "services/config:foo" 188 name: "services/config:foo"
130 url: "https://foo" 189 url: "https://foo"
131 } 190 }
132 schemas { 191 schemas {
133 name: "projects:foo" 192 name: "projects:foo"
134 url: "https://foo" 193 url: "https://foo"
135 } 194 }
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after
200 self.assertEqual( 259 self.assertEqual(
201 [m.text for m in result.messages], 260 [m.text for m in result.messages],
202 [ 261 [
203 'Ref #2: name is not specified', 262 'Ref #2: name is not specified',
204 'Ref #3: duplicate ref: refs/heads/master', 263 'Ref #3: duplicate ref: refs/heads/master',
205 'Ref #4: name does not start with "refs/": does_not_start_with_ref', 264 'Ref #4: name does not start with "refs/": does_not_start_with_ref',
206 'Ref #4: must not contain ".." or "." components: ../bad/path' 265 'Ref #4: must not contain ".." or "." components: ../bad/path'
207 ], 266 ],
208 ) 267 )
209 268
210 def test_endpoint_validate_async(self): 269 def test_validation_by_service_async(self):
211 cfg = '# a config' 270 cfg = '# a config'
212 cfg_b64 = base64.b64encode(cfg) 271 cfg_b64 = base64.b64encode(cfg)
213 272
214 self.mock(storage, 'get_self_config_async', mock.Mock()) 273 self.services = [
215 storage.get_self_config_async.return_value = future( 274 service_config_pb2.Service(id='a'),
216 service_config_pb2.ValidationCfg( 275 service_config_pb2.Service(id='b'),
217 rules=[ 276 service_config_pb2.Service(id='c'),
218 service_config_pb2.ValidationCfg.Rule( 277 ]
219 config_set='services/foo', 278
220 path='bar.cfg', 279 @ndb.tasklet
280 def get_metadata_async(service_id):
281 if service_id == 'a':
282 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata(
283 validation=service_config_pb2.Validator(
284 patterns=[service_config_pb2.ConfigPattern(
285 config_set='services/foo',
286 path='bar.cfg',
287 )],
221 url='https://bar.verifier', 288 url='https://bar.verifier',
222 ), 289 )
223 service_config_pb2.ValidationCfg.Rule( 290 ))
224 config_set='regex:projects/[^/]+', 291 if service_id == 'b':
225 path='regex:.+.\cfg', 292 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata(
293 validation=service_config_pb2.Validator(
294 patterns=[service_config_pb2.ConfigPattern(
295 config_set=r'regex:projects/[^/]+',
296 path=r'regex:.+\.cfg',
297 )],
226 url='https://bar2.verifier', 298 url='https://bar2.verifier',
227 ), 299 )))
228 service_config_pb2.ValidationCfg.Rule( 300 if service_id == 'c':
229 config_set='regex:.+', 301 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata(
230 path='regex:.+', 302 validation=service_config_pb2.Validator(
303 patterns=[service_config_pb2.ConfigPattern(
304 config_set=r'regex:.+',
305 path=r'regex:.+',
306 )],
231 url='https://ultimate.verifier', 307 url='https://ultimate.verifier',
232 ), 308 )))
233 ] 309 return None
234 )) 310 self.mock(services, 'get_metadata_async', mock.Mock())
311 services.get_metadata_async.side_effect = get_metadata_async
235 312
236 @ndb.tasklet 313 @ndb.tasklet
237 def json_request_async(url, **kwargs): 314 def json_request_async(url, **kwargs):
238 raise ndb.Return({ 315 raise ndb.Return({
239 'messages': [{ 316 'messages': [{
240 'text': 'OK from %s' % url, 317 'text': 'OK from %s' % url,
241 # default severity 318 # default severity
242 }], 319 }],
243 }) 320 })
244 321
(...skipping 12 matching lines...) Expand all
257 text='OK from https://ultimate.verifier', severity=logging.INFO) 334 text='OK from https://ultimate.verifier', severity=logging.INFO)
258 ]) 335 ])
259 net.json_request_async.assert_any_call( 336 net.json_request_async.assert_any_call(
260 'https://bar.verifier', 337 'https://bar.verifier',
261 method='POST', 338 method='POST',
262 payload={ 339 payload={
263 'config_set': 'services/foo', 340 'config_set': 'services/foo',
264 'path': 'bar.cfg', 341 'path': 'bar.cfg',
265 'content': cfg_b64, 342 'content': cfg_b64,
266 }, 343 },
267 scope='https://www.googleapis.com/auth/userinfo.email', 344 scope=net.EMAIL_SCOPE,
268 ) 345 )
269 net.json_request_async.assert_any_call( 346 net.json_request_async.assert_any_call(
270 'https://ultimate.verifier', 347 'https://ultimate.verifier',
271 method='POST', 348 method='POST',
272 payload={ 349 payload={
273 'config_set': 'services/foo', 350 'config_set': 'services/foo',
274 'path': 'bar.cfg', 351 'path': 'bar.cfg',
275 'content': cfg_b64, 352 'content': cfg_b64,
276 }, 353 },
277 scope='https://www.googleapis.com/auth/userinfo.email', 354 scope=net.EMAIL_SCOPE,
278 ) 355 )
279 356
280 ############################################################################ 357 ############################################################################
281 358
282 result = validation.validate_config('projects/foo', 'bar.cfg', cfg) 359 result = validation.validate_config('projects/foo', 'bar.cfg', cfg)
283 self.assertEqual( 360 self.assertEqual(
284 result.messages, 361 result.messages,
285 [ 362 [
286 validation_context.Message( 363 validation_context.Message(
287 text='OK from https://bar2.verifier', severity=logging.INFO), 364 text='OK from https://bar2.verifier', severity=logging.INFO),
288 validation_context.Message( 365 validation_context.Message(
289 text='OK from https://ultimate.verifier', severity=logging.INFO) 366 text='OK from https://ultimate.verifier', severity=logging.INFO)
290 ]) 367 ])
291 net.json_request_async.assert_any_call( 368 net.json_request_async.assert_any_call(
292 'https://bar2.verifier', 369 'https://bar2.verifier',
293 method='POST', 370 method='POST',
294 payload={ 371 payload={
295 'config_set': 'projects/foo', 372 'config_set': 'projects/foo',
296 'path': 'bar.cfg', 373 'path': 'bar.cfg',
297 'content': cfg_b64, 374 'content': cfg_b64,
298 }, 375 },
299 scope='https://www.googleapis.com/auth/userinfo.email', 376 scope=net.EMAIL_SCOPE,
300 ) 377 )
301 net.json_request_async.assert_any_call( 378 net.json_request_async.assert_any_call(
302 'https://ultimate.verifier', 379 'https://ultimate.verifier',
303 method='POST', 380 method='POST',
304 payload={ 381 payload={
305 'config_set': 'projects/foo', 382 'config_set': 'projects/foo',
306 'path': 'bar.cfg', 383 'path': 'bar.cfg',
307 'content': cfg_b64, 384 'content': cfg_b64,
308 }, 385 },
309 scope='https://www.googleapis.com/auth/userinfo.email', 386 scope=net.EMAIL_SCOPE,
310 ) 387 )
311 388
312 ############################################################################ 389 ############################################################################
313 # Error found 390 # Error found
314 391
315 net.json_request_async.side_effect = None 392 net.json_request_async.side_effect = None
316 net.json_request_async.return_value = ndb.Future() 393 net.json_request_async.return_value = ndb.Future()
317 net.json_request_async.return_value.set_result({ 394 net.json_request_async.return_value.set_result({
318 'messages': [{ 395 'messages': [{
319 'text': 'error', 396 'text': 'error',
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
365 'config_set: projects/baz/refs/x\n' 442 'config_set: projects/baz/refs/x\n'
366 'path: qux.cfg\n' 443 'path: qux.cfg\n'
367 'response: %r' % res), 444 'response: %r' % res),
368 ), 445 ),
369 ], 446 ],
370 ) 447 )
371 448
372 449
373 if __name__ == '__main__': 450 if __name__ == '__main__':
374 test_env.main() 451 test_env.main()
OLDNEW
« no previous file with comments | « appengine/config_service/validation.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698