def validate_definition(definition, deref, def_name=None): definition = deref(definition) if 'allOf' in definition: for inner_definition in definition['allOf']: validate_definition(inner_definition, deref) else: required = definition.get('required', []) props = iterkeys(definition.get('properties', {})) extra_props = list(set(required) - set(props)) if extra_props: raise SwaggerValidationError( "Required list has properties not defined: {}".format( extra_props)) validate_defaults_in_definition(definition, deref) if 'discriminator' in definition: required_props, not_required_props = get_collapsed_properties_type_mappings( definition, deref) discriminator = definition['discriminator'] if discriminator not in required_props and discriminator not in not_required_props: raise SwaggerValidationError( 'discriminator (%s) must be defined in properties' % discriminator) if discriminator not in required_props: raise SwaggerValidationError( 'discriminator (%s) must be defined a required property' % discriminator) if required_props[discriminator] != 'string': raise SwaggerValidationError( 'discriminator (%s) must be a string property' % discriminator)
def get_validator(spec_json, origin='unknown'): """ :param spec_json: Dict representation of the json API spec :param origin: filename or url of the spec - only use for error messages :return: module responsible for validation based on Swagger version in the spec """ swagger12_version = spec_json.get('swaggerVersion') swagger20_version = spec_json.get('swagger') if swagger12_version and swagger20_version: raise SwaggerValidationError( "You've got conflicting keys for the Swagger version in your spec. " "Expected `swaggerVersion` or `swagger`, but not both.") elif swagger12_version and swagger12_version == '1.2': # we don't care about versions prior to 1.2 return validator12 elif swagger20_version and swagger20_version == '2.0': return validator20 elif swagger12_version is None and swagger20_version is None: raise SwaggerValidationError( "Swagger spec {0} missing version. Expected " "`swaggerVersion` or `swagger`".format(origin)) else: raise SwaggerValidationError( 'Swagger version {0} not supported.'.format(swagger12_version or swagger20_version))
def validate_definition(definition, deref, def_name=None, visited_definitions_ids=None): """ :param visited_definitions_ids: set of ids of already visited definitions (after dereference) This is used to cut recursion in case of recursive definitions :type visited_definitions_ids: set """ definition = deref(definition) if visited_definitions_ids is not None: if id(definition) in visited_definitions_ids: return visited_definitions_ids.add(id(definition)) swagger_type = definition.get('type') if isinstance(swagger_type, list): # not valid Swagger; see https://github.com/OAI/OpenAPI-Specification/issues/458 raise SwaggerValidationError('In definition of {}, type must be a string; lists are not allowed ({})'.format(def_name or '(no name)', swagger_type)) if 'allOf' in definition: for idx, inner_definition in enumerate(definition['allOf']): validate_definition( definition=inner_definition, deref=deref, def_name='{}/{}'.format(def_name, str(idx)), visited_definitions_ids=visited_definitions_ids, ) else: required = definition.get('required', []) props = iterkeys(definition.get('properties', {})) extra_props = list(set(required) - set(props)) if extra_props: raise SwaggerValidationError( "In definition of {}, required list has properties not defined: {}.".format( def_name or '(no name)', extra_props, ) ) validate_defaults_in_definition(definition, deref) validate_arrays_in_definition(definition, def_name=def_name) for property_name, property_spec in iteritems(definition.get('properties', {})): validate_definition( definition=property_spec, deref=deref, def_name='{}/properties/{}'.format(def_name, property_name), visited_definitions_ids=visited_definitions_ids, ) if 'discriminator' in definition: required_props, not_required_props = get_collapsed_properties_type_mappings(definition, deref) discriminator = definition['discriminator'] if discriminator not in required_props and discriminator not in not_required_props: raise SwaggerValidationError('In definition of {}, discriminator ({}) must be defined in properties'.format(def_name or '(no name)', discriminator)) if discriminator not in required_props: raise SwaggerValidationError('In definition of {}, discriminator ({}) must be a required property'.format(def_name or '(no name)', discriminator)) if required_props[discriminator] != 'string': raise SwaggerValidationError('In definition of {}, discriminator ({}) must be a string property'.format(def_name or '(no name)', discriminator))
def validate_non_body_parameter(param, deref, def_name): if 'type' not in param: raise SwaggerValidationError( 'Non-Body parameter in `{def_name}` does not specify `type`.'. format(def_name=def_name), ) if param['type'] == 'array' and 'items' not in param: raise SwaggerValidationError( 'Non-Body array parameter in `{def_name}` does not specify `items`.' .format(def_name=def_name), )
def validate_operation(operation, model_ids): """Validate an Operation Object (§5.2.3).""" try: validate_data_type(operation, model_ids, allow_refs=False, allow_voids=True) except SwaggerValidationError as e: raise SwaggerValidationError( 'Operation "{}": {}'.format(operation['nickname'], str(e))) for parameter in operation['parameters']: try: validate_parameter(parameter, model_ids) except SwaggerValidationError as e: raise SwaggerValidationError( 'Operation "%s", parameter "%s": %s' % (operation['nickname'], parameter['name'], str(e)))
def validate_data_type(obj, model_ids, allow_arrays=True, allow_voids=False, allow_refs=True, allow_file=False): """Validate an object that contains a data type (§4.3.3). Params: - obj: the dictionary containing the data type to validate - model_ids: a list of model ids - allow_arrays: whether an array is permitted in the data type. This is used to prevent nested arrays. - allow_voids: whether a void type is permitted. This is used when validating Operation Objects (§5.2.3). - allow_refs: whether '$ref's are permitted. If true, then 'type's are not allowed to reference model IDs. """ typ = obj.get('type') ref = obj.get('$ref') # TODO Use a custom jsonschema.Validator to Validate defaultValue # enum, minimum, maximum, uniqueItems if typ is not None: if typ in PRIMITIVE_TYPES: return if allow_voids and typ == 'void': return if typ == 'array': if not allow_arrays: raise SwaggerValidationError('"array" not allowed') # Items Object (§4.3.4) items = obj.get('items') if items is None: raise SwaggerValidationError('"items" not found') validate_data_type(items, model_ids, allow_arrays=False) return if typ == 'File': if not allow_file: raise SwaggerValidationError( 'Type "File" is only valid for form parameters') return if typ in model_ids: if allow_refs: raise SwaggerValidationError( 'must use "$ref" for referencing "%s"' % typ) return raise SwaggerValidationError('unknown type "%s"' % typ) if ref is not None: if not allow_refs: raise SwaggerValidationError('"$ref" not allowed') if ref not in model_ids: raise SwaggerValidationError('unknown model id "%s"' % ref) return raise SwaggerValidationError('no "$ref" or "type" present')
def validate_arrays_in_definition(definition_spec, def_name=None): if definition_spec.get( 'type') == 'array' and 'items' not in definition_spec: raise SwaggerValidationError( 'Definition of type array must define `items` property{}.'.format( '' if not def_name else ' (definition {})'.format(def_name), ), )
def validate_model(model, model_name, model_ids): """Validate a Model Object (§5.2.7).""" # TODO Validate 'sub-types' and 'discriminator' fields for required in model.get('required', []): if required not in model['properties']: raise SwaggerValidationError( 'Model "%s": required property "%s" not found' % (model_name, required)) for prop_name, prop in model.get('properties', {}).iteritems(): try: validate_data_type(prop, model_ids, allow_refs=True) except SwaggerValidationError as e: # Add more context to the exception and re-raise raise SwaggerValidationError('Model "%s", property "%s": %s' % (model_name, prop_name, str(e)))
def validate_responses(api, http_verb, responses_dict): if is_ref(responses_dict): raise SwaggerValidationError( '{http_verb} {api} does not have a valid responses section. ' 'That section cannot be just a reference to another object.'.format( http_verb=http_verb.upper(), api=api, ) )
def validate_model(model, model_name, model_ids): """Validate a Model Object (§5.2.7).""" # TODO Validate 'sub-types' and 'discriminator' fields for required in model.get('required', []): if required not in model['properties']: raise SwaggerValidationError( 'Model "%s": required property "%s" not found' % (model_name, required)) if model_name != model['id']: error = 'model name: {} does not match model id: {}'.format(model_name, model['id']) raise SwaggerValidationError(error) for prop_name, prop in six.iteritems(model.get('properties', {})): try: validate_data_type(prop, model_ids, allow_refs=True) except SwaggerValidationError as e: # Add more context to the exception and re-raise raise SwaggerValidationError( 'Model "{}", property "{}": {}'.format(model_name, prop_name, str(e)))
def test_valid_openapi(): filename = "openapis/swagger.yaml" with codecs.open(filename, encoding="utf-8") as f: url = "file://" + filename + "#" spec = yaml.safe_load(f) if not isinstance(spec, dict): raise SwaggerValidationError("root node is not a mapping") # ensure the spec is valid JSON spec = json.loads(json.dumps(spec)) validator = swagger_spec_validator.util.get_validator(spec, url) validator.validate_spec(spec, url)
def validate_unresolvable_path_params(path_name, path_params): """Validate that every path parameter listed is also defined. :param path_name: complete path name as a string. :param path_params: Names of all the eligible path parameters :returns: `None` in case of success, otherwise raises an exception. :raises: :py:class:`swagger_spec_validator.SwaggerValidationError` """ msg = "Path Parameter used is not defined" for path in get_path_params_from_url(path_name): if path not in path_params: raise SwaggerValidationError("%s: %s" % (msg, path))
def validate_body_parameter(param, deref, def_name): if 'schema' not in param: raise SwaggerValidationError( 'Body parameter in `{def_name}` does not specify `schema`.'.format( def_name=def_name)) validate_definition( definition=param['schema'], deref=deref, def_name='{}/schema'.format(def_name), visited_definitions_ids=set(), )
def validate_unresolvable_path_params(path_name, path_params): """Validate that every path parameter listed is also defined. :param path_name: complete path name as a string. :param path_params: Names of all the eligible path parameters :raises: :py:class:`swagger_spec_validator.SwaggerValidationError` """ for path in get_path_params_from_url(path_name): if path not in path_params: msg = "Path parameter '{}' used is not documented on '{}'".format(path, path_name) raise SwaggerValidationError(msg)
def validate_duplicate_param(params): """Validate no duplicate parameters are present. Uniqueness is determined by the combination of 'name' and 'in'. :param params: list of all the params :returns: `None` in case of success, otherwise raises an exception. :raises: :py:class:`swagger_spec_validator.SwaggerValidationError` """ seen = set() msg = "Duplicate param found with (name, in)" for param in params: param_id = (param['name'], param['in']) if param_id in seen: raise SwaggerValidationError("%s: %s" % (msg, param_id)) seen.add(param_id)
def validate_definitions(definitions): """Validates the semantic errors in `definitions` of the Spec. :param apis: dict of all the definitions :returns: `None` in case of success, otherwise raises an exception. :raises: :py:class:`swagger_spec_validator.SwaggerValidationError` :raises: :py:class:`jsonschema.exceptions.ValidationError` """ for def_name in definitions: definition = definitions[def_name] required = definition.get('required', []) props = definition.get('properties', {}).keys() extra_props = list(set(required) - set(props)) msg = "Required list has properties not defined" if extra_props: raise SwaggerValidationError("%s: %s" % (msg, extra_props))
def validate_definitions(definitions, deref): """Validates the semantic errors in #/definitions. :param definitions: dict of all the definitions :param deref: callable that dereferences $refs :raises: :py:class:`swagger_spec_validator.SwaggerValidationError` :raises: :py:class:`jsonschema.exceptions.ValidationError` """ for def_name, definition in iteritems(definitions): definition = deref(definition) required = definition.get('required', []) props = definition.get('properties', {}).keys() extra_props = list(set(required) - set(props)) if extra_props: msg = "Required list has properties not defined" raise SwaggerValidationError("%s: %s" % (msg, extra_props))
def validate_arrays_in_definition(definition_spec, deref, def_name=None, visited_definitions=None): if definition_spec.get('type') == 'array': if 'items' not in definition_spec: raise SwaggerValidationError( 'Definition of type array must define `items` property{}.'. format( '' if not def_name else ' (definition {})'.format(def_name), ), ) validate_definition( definition=definition_spec['items'], deref=deref, def_name='{}/items'.format(def_name), visited_definitions=visited_definitions, )
def validate_duplicate_param(params, deref): """Validate no duplicate parameters are present. Uniqueness is determined by the tuple ('name', 'in'). :param params: list of all the params :param deref: callable that dereferences $refs :raises: :py:class:`swagger_spec_validator.SwaggerValidationError` when a duplicate parameter is found. """ seen = set() msg = "Duplicate param found with (name, in)" for param in params: param = deref(param) param_key = (param['name'], param['in']) if param_key in seen: raise SwaggerValidationError("%s: %s" % (msg, param_key)) seen.add(param_key)
def validate_responses(api, http_verb, responses_dict, deref=None): if is_ref(responses_dict): raise SwaggerValidationError( '{http_verb} {api} does not have a valid responses section. ' 'That section cannot be just a reference to another object.'.format( http_verb=http_verb.upper(), api=api, ) ) for response_status, response_spec in iteritems(responses_dict): response_schema = response_spec.get('schema') if response_schema is None: continue validate_definition( definition=response_schema, deref=deref, def_name='#/paths/{api}/{http_verb}/responses/{status_code}'.format( http_verb=http_verb, api=api, status_code=response_status, ), visited_definitions_ids=set(), )
def validate_apis(apis, deref): """Validates semantic errors in #/paths. :param apis: dict of all the #/paths :param deref: callable that dereferences $refs :raises: :py:class:`swagger_spec_validator.SwaggerValidationError` :raises: :py:class:`jsonschema.exceptions.ValidationError` """ operation_tag_to_operation_id_set = defaultdict(set) for api_name, api_body in iteritems(apis): api_body = deref(api_body) api_params = deref(api_body.get('parameters', [])) validate_duplicate_param(api_params, deref) for idx, param in enumerate(api_params): validate_parameter( param=param, deref=deref, def_name='#/paths/{api_name}/parameters/{idx}'.format( api_name=api_name, idx=idx, ), ) for oper_name in api_body: # don't treat parameters that apply to all api operations as # an operation if oper_name == 'parameters' or oper_name.startswith('x-'): continue oper_body = deref(api_body[oper_name]) oper_tags = deref(oper_body.get('tags', [None])) # Check that, if this operation has an operationId defined, # no other operation with a same tag also has that # operationId. operation_id = oper_body.get('operationId') if operation_id is not None: for oper_tag in oper_tags: if operation_id in operation_tag_to_operation_id_set[ oper_tag]: raise SwaggerValidationError( "Duplicate operationId: {}".format(operation_id)) operation_tag_to_operation_id_set[oper_tag].add( operation_id) oper_params = deref(oper_body.get('parameters', [])) validate_duplicate_param(oper_params, deref) all_path_params = list( set( get_path_param_names(api_params, deref) + get_path_param_names(oper_params, deref))) validate_unresolvable_path_params(api_name, all_path_params) for idx, param in enumerate(oper_params): validate_parameter( param=param, deref=deref, def_name='#/paths/{api_name}/{oper_name}/parameters/{idx}'. format( api_name=api_name, oper_name=oper_name, idx=idx, ), ) # Responses validation validate_responses(api_name, oper_name, oper_body['responses'], deref)
def _fuzz_parameter( parameter: Dict[str, Any], operation_id: str = None, required: bool = False, ) -> SearchStrategy: """ :param required: for object types, the required parameter is in a separate array, rather than being attached to each parameter object. This parameter allows objects to pass in this information. e.g. { 'type': 'object', 'required': [ 'name', ], 'properties': { 'name': { 'type': 'string', }, }, } """ required = parameter.get('required', required) _type = parameter.get('type') if not _type: raise SwaggerValidationError( 'Missing \'type\' from {}'.format( json.dumps(parameter), ), ) strategy = _get_strategy_from_factory(_type, operation_id, parameter.get('name')) if not strategy: if 'enum' in parameter: return st.sampled_from(parameter['enum']) # As per https://swagger.io/docs/specification/data-models/data-types, # there are only a limited set of data types. mapping = { 'string': _fuzz_string, 'number': _fuzz_number, 'integer': _fuzz_integer, 'boolean': _fuzz_boolean, 'array': _fuzz_array, 'object': _fuzz_object, # TODO: handle `file` type # https://swagger.io/docs/specification/2-0/file-upload/ } fuzz_fn = mapping[_type] if fuzz_fn in (_fuzz_object, _fuzz_array): strategy = fuzz_fn(parameter, operation_id, required=required) # type: ignore else: strategy = fuzz_fn(parameter, required=required) # type: ignore # NOTE: We don't currently support `nullable` values, so we use `None` as a # proxy to exclude the parameter from the final dictionary. if ( # `name` check is used here as a heuristic to determine whether in # recursive call (arrays). parameter.get('name') and not required ): return st.one_of(st.none(), strategy) # type: ignore return strategy # type: ignore
def validate_definition(definition, deref, def_name=None, visited_definitions=None): """ :param visited_definitions: set of already visited definitions This is used to cut recursion in case of recursive definitions :type visited_definitions: set """ if visited_definitions is not None: # Remove x-scope or else no two definitions will be the same stripped_definition = json.dumps( {key: definition[key] for key in definition if key != 'x-scope'}, sort_keys=True) if stripped_definition in visited_definitions: return visited_definitions.add(stripped_definition) definition = deref(definition) swagger_type = definition.get('type') if isinstance(swagger_type, list): # not valid Swagger; see https://github.com/OAI/OpenAPI-Specification/issues/458 raise SwaggerValidationError( 'In definition of {}, type must be a string; lists are not allowed ({})' .format(def_name or '(no name)', swagger_type)) if 'allOf' in definition: for idx, inner_definition in enumerate(definition['allOf']): validate_definition( definition=inner_definition, deref=deref, def_name='{}/{}'.format(def_name, str(idx)), visited_definitions=visited_definitions, ) else: required = definition.get('required', []) props = iterkeys(definition.get('properties', {})) extra_props = list(set(required) - set(props)) if extra_props: raise SwaggerValidationError( "In definition of {}, required list has properties not defined: {}." .format( def_name or '(no name)', extra_props, )) validate_defaults_in_definition(definition, deref) validate_arrays_in_definition(definition_spec=definition, deref=deref, def_name=def_name, visited_definitions=visited_definitions) for property_name, property_spec in iteritems( definition.get('properties', {})): validate_definition( definition=property_spec, deref=deref, def_name='{}/properties/{}'.format(def_name, property_name), visited_definitions=visited_definitions, ) if 'additionalProperties' in definition: if definition.get('additionalProperties') not in (True, False): validate_definition( definition=definition.get('additionalProperties'), deref=deref, def_name='{}/additionalProperties'.format(def_name), visited_definitions=visited_definitions, ) if 'discriminator' in definition: required_props, not_required_props = get_collapsed_properties_type_mappings( definition, deref) discriminator = definition['discriminator'] if discriminator not in required_props and discriminator not in not_required_props: raise SwaggerValidationError( 'In definition of {}, discriminator ({}) must be defined in properties' .format(def_name or '(no name)', discriminator)) if discriminator not in required_props: raise SwaggerValidationError( 'In definition of {}, discriminator ({}) must be a required property' .format(def_name or '(no name)', discriminator)) if required_props[discriminator] != 'string': raise SwaggerValidationError( 'In definition of {}, discriminator ({}) must be a string property' .format(def_name or '(no name)', discriminator))
def validate_url(url_string, qualifying=("scheme", "netloc")): tokens = urlparse(url_string) if not all([getattr(tokens, qual_attr) for qual_attr in qualifying]): raise SwaggerValidationError(f"{url_string} invalid")
def validate_email(email_string): d = is_email(email_string, diagnose=True) if d > BaseDiagnosis.CATEGORIES["VALID"]: raise SwaggerValidationError(f"{email_string} {d.message}")