Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2015 The Swarming Authors. All rights reserved. | 1 # Copyright 2015 The Swarming Authors. All rights reserved. |
| 2 # Use of this source code is governed by the Apache v2.0 license that can be | 2 # Use of this source code is governed by the Apache v2.0 license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import base64 | 5 import base64 |
| 6 import logging | 6 import logging |
| 7 import posixpath | 7 import posixpath |
| 8 import re | 8 import re |
| 9 import urlparse | 9 import urlparse |
| 10 | 10 |
| 11 from google.appengine.ext import ndb | 11 from google.appengine.ext import ndb |
| 12 | 12 |
| 13 from components import auth | |
| 13 from components import config | 14 from components import config |
| 14 from components import gitiles | 15 from components import gitiles |
| 15 from components import net | 16 from components import net |
| 16 from components.config import validation | 17 from components.config import validation |
| 17 | 18 |
| 18 from proto import project_config_pb2 | 19 from proto import project_config_pb2 |
| 19 from proto import service_config_pb2 | 20 from proto import service_config_pb2 |
| 20 import common | 21 import common |
| 22 import services | |
| 21 import storage | 23 import storage |
| 22 | 24 |
| 23 | 25 |
| 24 def validate_config_set(config_set, ctx=None): | 26 def validate_config_set(config_set, ctx=None): |
| 25 ctx = ctx or validation.Context.raise_on_error() | 27 ctx = ctx or validation.Context.raise_on_error() |
| 26 if not any(r.match(config_set) for r in config.ALL_CONFIG_SET_RGX): | 28 if not any(r.match(config_set) for r in config.ALL_CONFIG_SET_RGX): |
| 27 ctx.error('invalid config set: %s', config_set) | 29 ctx.error('invalid config set: %s', config_set) |
| 28 | 30 |
| 29 | 31 |
| 30 def validate_path(path, ctx=None): | 32 def validate_path(path, ctx=None): |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 43 ctx.error('not specified') | 45 ctx.error('not specified') |
| 44 return | 46 return |
| 45 parsed = urlparse.urlparse(url) | 47 parsed = urlparse.urlparse(url) |
| 46 if not parsed.netloc: | 48 if not parsed.netloc: |
| 47 ctx.error('hostname not specified') | 49 ctx.error('hostname not specified') |
| 48 if parsed.scheme != 'https': | 50 if parsed.scheme != 'https': |
| 49 ctx.error('scheme must be "https"') | 51 ctx.error('scheme must be "https"') |
| 50 | 52 |
| 51 | 53 |
| 52 def validate_pattern(pattern, literal_validator, ctx): | 54 def validate_pattern(pattern, literal_validator, ctx): |
| 55 if not isinstance(pattern, basestring): | |
| 56 ctx.error('pattern must be a string: %r', pattern) | |
| 57 return | |
| 53 if ':' not in pattern: | 58 if ':' not in pattern: |
|
Sergiy Byelozyorov
2015/07/07 09:10:47
a lot of this code is duplicated in compile_patter
nodir
2015/07/07 15:54:18
Done.
| |
| 54 literal_validator(pattern, ctx) | 59 kind = 'text' |
| 60 value = pattern | |
| 61 else: | |
| 62 kind, value = pattern.split(':', 2) | |
| 63 | |
| 64 if kind == 'text': | |
| 65 literal_validator(value, ctx) | |
| 55 return | 66 return |
| 56 | 67 |
| 57 pattern_type, pattern_text = pattern.split(':', 2) | 68 if kind == 'regex': |
| 58 if pattern_type != 'regex': | 69 try: |
| 59 ctx.error('unknown pattern type: %s', pattern_type) | 70 re.compile(value) |
| 71 except re.error as ex: | |
| 72 ctx.error('invalid regular expression "%s": %s', value, ex) | |
| 60 return | 73 return |
| 61 try: | 74 |
| 62 re.compile(pattern_text) | 75 ctx.error('unknown pattern kind: %s', kind) |
| 63 except re.error as ex: | |
| 64 ctx.error('invalid regular expression "%s": %s', pattern_text, ex) | |
| 65 | 76 |
| 66 | 77 |
| 67 @validation.self_rule( | 78 def check_id_sorted(iterable, list_name, ctx): |
| 68 common.VALIDATION_FILENAME, service_config_pb2.ValidationCfg) | 79 """Emits a warning if the iterable is not sorted by id.""" |
| 69 def validate_validation_cfg(cfg, ctx): | 80 prev = None |
| 70 for i, rule in enumerate(cfg.rules): | 81 for item in iterable: |
| 71 with ctx.prefix('Rule #%d: ', i + 1): | 82 if not item.id: |
| 72 with ctx.prefix('config_set: '): | 83 continue |
| 73 validate_pattern(rule.config_set, validate_config_set, ctx) | 84 if prev is not None and item.id < prev: |
| 74 with ctx.prefix('path: '): | 85 ctx.warning( |
| 75 validate_pattern(rule.path, validate_path, ctx) | 86 '%s are not sorted by id. First offending id: %s', list_name, item.id) |
| 76 with ctx.prefix('url: '): | 87 return |
| 77 validate_url(rule.url, ctx) | 88 prev = item.id |
| 89 | |
| 90 | |
| 91 def validate_id(id, rgx, known_ids, ctx): | |
| 92 if not id: | |
| 93 ctx.error('id is not specified') | |
| 94 return | |
| 95 if not rgx.match(id): | |
| 96 ctx.error('id "%s" does not match %s regex', id, rgx.pattern) | |
| 97 return | |
| 98 if id in known_ids: | |
| 99 ctx.error('id is not unique') | |
| 100 else: | |
| 101 known_ids.add(id) | |
| 78 | 102 |
| 79 | 103 |
| 80 def validate_config_set_location(loc, ctx, allow_relative_url=False): | 104 def validate_config_set_location(loc, ctx, allow_relative_url=False): |
| 81 if not loc: | 105 if not loc: |
| 82 ctx.error('not specified') | 106 ctx.error('not specified') |
| 83 return | 107 return |
| 84 if loc.storage_type == service_config_pb2.ConfigSetLocation.UNSET: | 108 if allow_relative_url and is_url_relative(loc.url): |
| 109 if loc.storage_type != service_config_pb2.ConfigSetLocation.UNSET: | |
| 110 ctx.error('storage_type must not be set if relative url is used') | |
| 111 elif loc.storage_type == service_config_pb2.ConfigSetLocation.UNSET: | |
|
Sergiy Byelozyorov
2015/07/07 09:10:47
I would also add
elif not allow_relative_url and
nodir
2015/07/07 15:54:18
gitiles.Location.parse ensures that the url is not
Sergiy Byelozyorov
2015/07/07 23:04:38
Acknowledged.
| |
| 85 ctx.error('storage_type is not set') | 112 ctx.error('storage_type is not set') |
| 86 else: | 113 else: |
| 87 assert loc.storage_type == service_config_pb2.ConfigSetLocation.GITILES | 114 assert loc.storage_type == service_config_pb2.ConfigSetLocation.GITILES |
| 88 if allow_relative_url and is_url_relative(loc.url): | |
| 89 # It is relative. Avoid calling gitiles.Location.parse. | |
| 90 return | |
| 91 try: | 115 try: |
| 92 gitiles.Location.parse(loc.url) | 116 gitiles.Location.parse(loc.url) |
| 93 except ValueError as ex: | 117 except ValueError as ex: |
| 94 ctx.error(ex.message) | 118 ctx.error(ex.message) |
| 95 | 119 |
| 96 | 120 |
| 97 @validation.self_rule( | 121 @validation.self_rule( |
| 98 common.PROJECT_REGISTRY_FILENAME, service_config_pb2.ProjectsCfg) | 122 common.PROJECT_REGISTRY_FILENAME, service_config_pb2.ProjectsCfg) |
| 99 def validate_project_registry(cfg, ctx): | 123 def validate_project_registry(cfg, ctx): |
| 100 project_ids = set() | 124 project_ids = set() |
| 101 unsorted_id = None | |
| 102 for i, project in enumerate(cfg.projects): | 125 for i, project in enumerate(cfg.projects): |
| 103 with ctx.prefix('Project %s: ', project.id or ('#%d' % (i + 1))): | 126 with ctx.prefix('Project %s: ', project.id or ('#%d' % (i + 1))): |
| 104 if not project.id: | 127 validate_id(project.id, config.common.PROJECT_ID_RGX, project_ids, ctx) |
| 105 ctx.error('id is not specified') | |
| 106 else: | |
| 107 if project.id in project_ids: | |
| 108 ctx.error('id is not unique') | |
| 109 else: | |
| 110 project_ids.add(project.id) | |
| 111 if not unsorted_id and i > 0: | |
| 112 if cfg.projects[i - 1].id and project.id < cfg.projects[i - 1].id: | |
| 113 unsorted_id = project.id | |
| 114 with ctx.prefix('config_location: '): | 128 with ctx.prefix('config_location: '): |
| 115 validate_config_set_location(project.config_location, ctx) | 129 validate_config_set_location(project.config_location, ctx) |
| 130 check_id_sorted(cfg.projects, 'Projects', ctx) | |
| 116 | 131 |
| 117 | 132 |
| 118 if unsorted_id: | 133 def validate_email(email, ctx): |
| 119 ctx.warning( | 134 try: |
| 120 'Project list is not sorted by id. First offending id: %s', | 135 auth.Identity('user', email) |
|
Sergiy Byelozyorov
2015/07/07 09:10:47
I think this is a misuse of the auth.Identity.__ne
nodir
2015/07/07 15:54:18
This is exactly what I did in a previous CL. Vadim
Sergiy Byelozyorov
2015/07/07 23:04:38
Acknowledged.
| |
| 121 unsorted_id) | 136 except ValueError as ex: |
| 137 ctx.error('invalid email: "%s"', email) | |
| 138 | |
| 139 | |
| 140 @validation.self_rule( | |
| 141 common.SERVICES_REGISTRY_FILENAME, service_config_pb2.ServicesCfg) | |
| 142 def validate_services_cfg(cfg, ctx): | |
| 143 service_ids = set() | |
| 144 for i, service in enumerate(cfg.services): | |
| 145 with ctx.prefix('Service %s: ', service.id or ('#%d' % (i + 1))): | |
| 146 validate_id(service.id, config.common.SERVICE_ID_RGX, service_ids, ctx) | |
| 147 if service.config_location and service.config_location.url: | |
| 148 with ctx.prefix('config_location: '): | |
| 149 validate_config_set_location( | |
| 150 service.config_location, ctx, allow_relative_url=True) | |
| 151 for owner in service.owners: | |
| 152 validate_email(owner, ctx) | |
| 153 | |
| 154 if service.metadata_url: | |
| 155 with ctx.prefix('metadata_url: '): | |
| 156 validate_url(service.metadata_url, ctx) | |
| 157 | |
| 158 check_id_sorted(cfg.services, 'Services', ctx) | |
| 159 | |
| 160 | |
| 161 def validate_service_dynamic_metadata_blob(metadata, ctx): | |
| 162 """Validates JSON-encoded ServiceDynamicMetadata""" | |
| 163 if not isinstance(metadata, dict): | |
| 164 ctx.error('Service dynamic metadata must be an object') | |
| 165 return | |
| 166 | |
| 167 validation = metadata.get('validation') | |
| 168 if validation is None: | |
| 169 return | |
| 170 | |
| 171 with ctx.prefix('validation: '): | |
| 172 if not isinstance(validation, dict): | |
| 173 ctx.error('must be an object') | |
| 174 return | |
| 175 with ctx.prefix('url: '): | |
| 176 validate_url(validation.get('url'), ctx) | |
| 177 patterns = validation.get('patterns') | |
| 178 if not isinstance(patterns, list): | |
| 179 ctx.error('patterns must be a list') | |
| 180 return | |
| 181 for i, p in enumerate(patterns): | |
| 182 with ctx.prefix('pattern #%d: ', i + 1): | |
| 183 if not isinstance(p, dict): | |
| 184 ctx.error('must be an object') | |
| 185 continue | |
| 186 with ctx.prefix('config_set: '): | |
| 187 validate_pattern(p.get('config_set'), validate_config_set, ctx) | |
| 188 with ctx.prefix('path: '): | |
| 189 validate_pattern(p.get('path'), validate_path, ctx) | |
| 122 | 190 |
| 123 | 191 |
| 124 @validation.self_rule(common.ACL_FILENAME, service_config_pb2.AclCfg) | 192 @validation.self_rule(common.ACL_FILENAME, service_config_pb2.AclCfg) |
| 125 def validate_acl_cfg(_cfg, _ctx): | 193 def validate_acl_cfg(_cfg, _ctx): |
| 126 # A valid protobuf message is enough. | 194 # A valid protobuf message is enough. |
| 127 pass | 195 pass |
| 128 | 196 |
| 129 @validation.self_rule(common.IMPORT_FILENAME, service_config_pb2.ImportCfg) | 197 @validation.self_rule(common.IMPORT_FILENAME, service_config_pb2.ImportCfg) |
| 130 def validate_import_cfg(_cfg, _ctx): | 198 def validate_import_cfg(_cfg, _ctx): |
| 131 # A valid protobuf message is enough. | 199 # A valid protobuf message is enough. |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 177 ctx.error('name does not start with "refs/": %s', ref.name) | 245 ctx.error('name does not start with "refs/": %s', ref.name) |
| 178 elif ref.name in refs: | 246 elif ref.name in refs: |
| 179 ctx.error('duplicate ref: %s', ref.name) | 247 ctx.error('duplicate ref: %s', ref.name) |
| 180 else: | 248 else: |
| 181 refs.add(ref.name) | 249 refs.add(ref.name) |
| 182 if ref.config_path: | 250 if ref.config_path: |
| 183 validate_path(ref.config_path, ctx) | 251 validate_path(ref.config_path, ctx) |
| 184 | 252 |
| 185 | 253 |
| 186 @ndb.tasklet | 254 @ndb.tasklet |
| 187 def _endpoint_validate_async(url, config_set, path, content, ctx): | 255 def _validate_by_service_async(service, config_set, path, content, ctx): |
| 188 """Validates a config with an external service.""" | 256 """Validates a config with an external service.""" |
| 257 try: | |
| 258 metadata = yield services.get_metadata_async(service.id) | |
| 259 except services.DynamicMetadataError as ex: | |
| 260 logging.error('Could not load dynamic metadata for %s: %s', service.id, ex) | |
| 261 return | |
| 262 | |
| 263 assert metadata and metadata.validation | |
| 264 url = metadata.validation.url | |
| 265 if not url: | |
| 266 return | |
| 267 | |
| 268 match = False | |
| 269 for p in metadata.validation.patterns: | |
| 270 # TODO(nodir): optimize if necessary. | |
| 271 if (validation.compile_pattern(p.config_set)(config_set) and | |
| 272 validation.compile_pattern(p.path)(path)): | |
| 273 match = True | |
| 274 break | |
| 275 if not match: | |
| 276 return | |
| 277 | |
| 189 res = None | 278 res = None |
| 190 | 279 |
| 191 def report_error(text): | 280 def report_error(text): |
| 192 text = ( | 281 text = ( |
| 193 'Error during external validation: %s\n' | 282 'Error during external validation: %s\n' |
| 194 'url: %s\n' | 283 'url: %s\n' |
| 195 'config_set: %s\n' | 284 'config_set: %s\n' |
| 196 'path: %s\n' | 285 'path: %s\n' |
| 197 'response: %r') % (text, url, config_set, path, res) | 286 'response: %r') % (text, url, config_set, path, res) |
| 198 logging.error(text) | 287 logging.error(text) |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 238 | 327 |
| 239 Returns: | 328 Returns: |
| 240 components.config.validation_context.Result. | 329 components.config.validation_context.Result. |
| 241 """ | 330 """ |
| 242 ctx = ctx or validation.Context() | 331 ctx = ctx or validation.Context() |
| 243 | 332 |
| 244 # Check the config against built-in validators, | 333 # Check the config against built-in validators, |
| 245 # defined using validation.self_rule. | 334 # defined using validation.self_rule. |
| 246 validation.validate(config_set, path, content, ctx=ctx) | 335 validation.validate(config_set, path, content, ctx=ctx) |
| 247 | 336 |
| 248 validation_cfg = yield storage.get_self_config_async( | 337 all_services = yield services.get_services_async() |
| 249 common.VALIDATION_FILENAME, service_config_pb2.ValidationCfg) | |
| 250 # Be paranoid, check yourself. | |
| 251 validate_validation_cfg(validation_cfg, validation.Context.raise_on_error()) | |
| 252 | |
| 253 futures = [] | 338 futures = [] |
| 254 for rule in validation_cfg.rules: | 339 for service in all_services: |
| 255 if (_pattern_match(rule.config_set, config_set) and | 340 futures.append( |
| 256 _pattern_match(rule.path, path)): | 341 _validate_by_service_async(service, config_set, path, content, ctx)) |
| 257 futures.append( | |
| 258 _endpoint_validate_async(rule.url, config_set, path, content, ctx)) | |
| 259 yield futures | 342 yield futures |
| 260 raise ndb.Return(ctx.result()) | 343 raise ndb.Return(ctx.result()) |
| 261 | 344 |
| 262 | 345 |
| 263 def validate_config(*args, **kwargs): | 346 def validate_config(*args, **kwargs): |
| 264 """Blocking version of validate_async.""" | 347 """Blocking version of validate_async.""" |
| 265 return validate_config_async(*args, **kwargs).get_result() | 348 return validate_config_async(*args, **kwargs).get_result() |
| 266 | 349 |
| 267 | 350 |
| 268 def _pattern_match(pattern, value): | |
| 269 # Assume pattern is valid. | |
| 270 if ':' not in pattern: | |
| 271 return pattern == value | |
| 272 else: | |
| 273 kind, pattern = pattern.split(':', 2) | |
| 274 assert kind == 'regex' | |
| 275 return bool(re.match('^%s$' % pattern, value)) | |
| 276 | |
| 277 | |
| 278 def is_url_relative(url): | 351 def is_url_relative(url): |
| 279 parsed = urlparse.urlparse(url) | 352 parsed = urlparse.urlparse(url) |
| 280 return bool(not parsed.scheme and not parsed.netloc and parsed.path) | 353 return bool(not parsed.scheme and not parsed.netloc and parsed.path) |
| OLD | NEW |