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

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: fine-grained acls for service 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
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 }
83 services {
84 owners: "not an email"
85 config_location {
86 storage_type: GITILES
87 url: "../some"
88 }
89 metadata_url: "not an url"
90 access: "**&"
91 }
92 services {
93 id: "b"
94 config_location {
95 storage_type: GITILES
96 url: "https://gitiles.host.com/project"
97 }
98 }
99 services {
100 id: "a-unsorted"
101 }
102 '''
103 result = validation.validate_config(
104 config.self_config_set(), 'services.cfg', cfg)
105
106 self.assertEqual(
107 [m.text for m in result.messages],
108 [
109 'Service #2: id is not specified',
110 ('Service #2: config_location: '
111 'storage_type must not be set if relative url is used'),
112 'Service #2: invalid email: "not an email"',
113 'Service #2: metadata_url: hostname not specified',
114 'Service #2: metadata_url: scheme must be "https"',
115 'Service #2: invalid group: **&',
116 'Services are not sorted by id. First offending id: a-unsorted',
117 ]
118 )
119
120
121 def test_validate_service_dynamic_metadata_blob(self):
122 def expect_errors(blob, expected_messages):
123 ctx = config.validation.Context()
124 validation.validate_service_dynamic_metadata_blob(blob, ctx)
125 self.assertEqual(
126 [m.text for m in ctx.result().messages], expected_messages)
127
128 expect_errors([], ['Service dynamic metadata must be an object'])
129 expect_errors({}, [])
130 expect_errors({'validation': 'bad'}, ['validation: must be an object'])
131 expect_errors(
132 {
133 'validation': {
134 'patterns': 'bad',
135 }
136 },
137 [
138 'validation: url: not specified',
139 'validation: patterns must be a list',
140 ])
141 expect_errors(
142 {
143 'validation': {
144 'url': 'bad url',
145 'patterns': [
146 'bad',
147 {
148 },
149 {
150 'config_set': 'a:b',
151 'path': '/foo',
152 },
153 {
154 'config_set': 'regex:)(',
155 'path': '../b',
156 },
157 {
158 'config_set': 'projects/foo',
159 'path': 'bar.cfg',
160 },
161 ]
162 }
163 },
164 [
165 'validation: url: hostname not specified',
166 'validation: url: scheme must be "https"',
167 'validation: pattern #1: must be an object',
168 'validation: pattern #2: config_set: Pattern must be a string',
169 'validation: pattern #2: path: Pattern must be a string',
170 'validation: pattern #3: config_set: Invalid pattern kind: a',
171 'validation: pattern #3: path: must not be absolute: /foo',
172 'validation: pattern #4: config_set: unbalanced parenthesis',
173 ('validation: pattern #4: path: '
174 'must not contain ".." or "." components: ../b'),
175 ]
176 )
177
126 def test_validate_schemas(self): 178 def test_validate_schemas(self):
127 cfg = ''' 179 cfg = '''
128 schemas { 180 schemas {
129 name: "services/config:foo" 181 name: "services/config:foo"
130 url: "https://foo" 182 url: "https://foo"
131 } 183 }
132 schemas { 184 schemas {
133 name: "projects:foo" 185 name: "projects:foo"
134 url: "https://foo" 186 url: "https://foo"
135 } 187 }
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after
200 self.assertEqual( 252 self.assertEqual(
201 [m.text for m in result.messages], 253 [m.text for m in result.messages],
202 [ 254 [
203 'Ref #2: name is not specified', 255 'Ref #2: name is not specified',
204 'Ref #3: duplicate ref: refs/heads/master', 256 'Ref #3: duplicate ref: refs/heads/master',
205 'Ref #4: name does not start with "refs/": does_not_start_with_ref', 257 'Ref #4: name does not start with "refs/": does_not_start_with_ref',
206 'Ref #4: must not contain ".." or "." components: ../bad/path' 258 'Ref #4: must not contain ".." or "." components: ../bad/path'
207 ], 259 ],
208 ) 260 )
209 261
210 def test_endpoint_validate_async(self): 262 def test_validation_by_service_async(self):
211 cfg = '# a config' 263 cfg = '# a config'
212 cfg_b64 = base64.b64encode(cfg) 264 cfg_b64 = base64.b64encode(cfg)
213 265
214 self.mock(storage, 'get_self_config_async', mock.Mock()) 266 self.services = [
215 storage.get_self_config_async.return_value = future( 267 service_config_pb2.Service(id='a'),
216 service_config_pb2.ValidationCfg( 268 service_config_pb2.Service(id='b'),
217 rules=[ 269 service_config_pb2.Service(id='c'),
218 service_config_pb2.ValidationCfg.Rule( 270 ]
219 config_set='services/foo', 271
220 path='bar.cfg', 272 @ndb.tasklet
273 def get_metadata_async(service_id):
274 if service_id == 'a':
275 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata(
276 validation=service_config_pb2.Validator(
277 patterns=[service_config_pb2.ConfigPattern(
278 config_set='services/foo',
279 path='bar.cfg',
280 )],
221 url='https://bar.verifier', 281 url='https://bar.verifier',
222 ), 282 )
223 service_config_pb2.ValidationCfg.Rule( 283 ))
224 config_set='regex:projects/[^/]+', 284 if service_id == 'b':
225 path='regex:.+.\cfg', 285 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata(
286 validation=service_config_pb2.Validator(
287 patterns=[service_config_pb2.ConfigPattern(
288 config_set=r'regex:projects/[^/]+',
289 path=r'regex:.+\.cfg',
290 )],
226 url='https://bar2.verifier', 291 url='https://bar2.verifier',
227 ), 292 )))
228 service_config_pb2.ValidationCfg.Rule( 293 if service_id == 'c':
229 config_set='regex:.+', 294 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata(
230 path='regex:.+', 295 validation=service_config_pb2.Validator(
296 patterns=[service_config_pb2.ConfigPattern(
297 config_set=r'regex:.+',
298 path=r'regex:.+',
299 )],
231 url='https://ultimate.verifier', 300 url='https://ultimate.verifier',
232 ), 301 )))
233 ] 302 return None
234 )) 303 self.mock(services, 'get_metadata_async', mock.Mock())
304 services.get_metadata_async.side_effect = get_metadata_async
235 305
236 @ndb.tasklet 306 @ndb.tasklet
237 def json_request_async(url, **kwargs): 307 def json_request_async(url, **kwargs):
238 raise ndb.Return({ 308 raise ndb.Return({
239 'messages': [{ 309 'messages': [{
240 'text': 'OK from %s' % url, 310 'text': 'OK from %s' % url,
241 # default severity 311 # default severity
242 }], 312 }],
243 }) 313 })
244 314
(...skipping 12 matching lines...) Expand all
257 text='OK from https://ultimate.verifier', severity=logging.INFO) 327 text='OK from https://ultimate.verifier', severity=logging.INFO)
258 ]) 328 ])
259 net.json_request_async.assert_any_call( 329 net.json_request_async.assert_any_call(
260 'https://bar.verifier', 330 'https://bar.verifier',
261 method='POST', 331 method='POST',
262 payload={ 332 payload={
263 'config_set': 'services/foo', 333 'config_set': 'services/foo',
264 'path': 'bar.cfg', 334 'path': 'bar.cfg',
265 'content': cfg_b64, 335 'content': cfg_b64,
266 }, 336 },
267 scope='https://www.googleapis.com/auth/userinfo.email', 337 scope=net.EMAIL_SCOPE,
268 ) 338 )
269 net.json_request_async.assert_any_call( 339 net.json_request_async.assert_any_call(
270 'https://ultimate.verifier', 340 'https://ultimate.verifier',
271 method='POST', 341 method='POST',
272 payload={ 342 payload={
273 'config_set': 'services/foo', 343 'config_set': 'services/foo',
274 'path': 'bar.cfg', 344 'path': 'bar.cfg',
275 'content': cfg_b64, 345 'content': cfg_b64,
276 }, 346 },
277 scope='https://www.googleapis.com/auth/userinfo.email', 347 scope=net.EMAIL_SCOPE,
278 ) 348 )
279 349
280 ############################################################################ 350 ############################################################################
281 351
282 result = validation.validate_config('projects/foo', 'bar.cfg', cfg) 352 result = validation.validate_config('projects/foo', 'bar.cfg', cfg)
283 self.assertEqual( 353 self.assertEqual(
284 result.messages, 354 result.messages,
285 [ 355 [
286 validation_context.Message( 356 validation_context.Message(
287 text='OK from https://bar2.verifier', severity=logging.INFO), 357 text='OK from https://bar2.verifier', severity=logging.INFO),
288 validation_context.Message( 358 validation_context.Message(
289 text='OK from https://ultimate.verifier', severity=logging.INFO) 359 text='OK from https://ultimate.verifier', severity=logging.INFO)
290 ]) 360 ])
291 net.json_request_async.assert_any_call( 361 net.json_request_async.assert_any_call(
292 'https://bar2.verifier', 362 'https://bar2.verifier',
293 method='POST', 363 method='POST',
294 payload={ 364 payload={
295 'config_set': 'projects/foo', 365 'config_set': 'projects/foo',
296 'path': 'bar.cfg', 366 'path': 'bar.cfg',
297 'content': cfg_b64, 367 'content': cfg_b64,
298 }, 368 },
299 scope='https://www.googleapis.com/auth/userinfo.email', 369 scope=net.EMAIL_SCOPE,
300 ) 370 )
301 net.json_request_async.assert_any_call( 371 net.json_request_async.assert_any_call(
302 'https://ultimate.verifier', 372 'https://ultimate.verifier',
303 method='POST', 373 method='POST',
304 payload={ 374 payload={
305 'config_set': 'projects/foo', 375 'config_set': 'projects/foo',
306 'path': 'bar.cfg', 376 'path': 'bar.cfg',
307 'content': cfg_b64, 377 'content': cfg_b64,
308 }, 378 },
309 scope='https://www.googleapis.com/auth/userinfo.email', 379 scope=net.EMAIL_SCOPE,
310 ) 380 )
311 381
312 ############################################################################ 382 ############################################################################
313 # Error found 383 # Error found
314 384
315 net.json_request_async.side_effect = None 385 net.json_request_async.side_effect = None
316 net.json_request_async.return_value = ndb.Future() 386 net.json_request_async.return_value = ndb.Future()
317 net.json_request_async.return_value.set_result({ 387 net.json_request_async.return_value.set_result({
318 'messages': [{ 388 'messages': [{
319 'text': 'error', 389 'text': 'error',
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
365 'config_set: projects/baz/refs/x\n' 435 'config_set: projects/baz/refs/x\n'
366 'path: qux.cfg\n' 436 'path: qux.cfg\n'
367 'response: %r' % res), 437 'response: %r' % res),
368 ), 438 ),
369 ], 439 ],
370 ) 440 )
371 441
372 442
373 if __name__ == '__main__': 443 if __name__ == '__main__':
374 test_env.main() 444 test_env.main()
OLDNEW
« appengine/config_service/acl.py ('K') | « appengine/config_service/validation.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698