def prune_unused_tags(swagger): """Prune the swagger (in place) of its unused tags""" if "tags" not in swagger: return swagger, [] tags_jspath = JSPATH_OPERATION_TAGS # detect security definitions used and for which scope tags_used = set().union(*[tags_list for _, tags_list, _ in get_elements(swagger, tags_jspath)]) # iterate existing securityDefinitions to check if they are used and if their scopes are used actions = [] for _, tag_name, (*path_before_name, path_name) in get_elements(swagger, JSPATH_TAGS): if tag_name not in tags_used: actions.append( TagNotUsedFilterAction( path=tuple(path_before_name), reason=f"tag definition for '{tag_name}' not used" ) ) swagger["tags"] = [tag for tag in swagger["tags"] if tag["name"] in tags_used] # remove tags if empty if not swagger["tags"]: del swagger["tags"] return swagger, actions
def detect_duplicate_operationId(swagger: Dict): """Return list of Action with duplicate operationIds""" events = set() # retrieve all operationIds operationId_jspath = JSPATH_OPERATIONID def get_operationId_name(name_value_path): return name_value_path[1] operationIds = sorted(get_elements(swagger, operationId_jspath), key=get_operationId_name) for opId, key_pths in groupby(operationIds, key=get_operationId_name): pths = tuple(subpth for _, _, subpth in key_pths) if len(pths) > 1: pth_first, *pths = pths for pth in pths: events.add( DuplicateOperationIdValidationError( path=pth, path_already_used=pth_first, reason=f"the operationId '{opId}' is already used in an endpoint.", operationId=opId, ) ) return events
def prune_unused_global_items(swagger): """Prune the swagger (in place) of its unused global items in the definitions, responses and parameters global sections""" def decompose_reference(references): return set( tuple(reference[2:].split("/")) for _, reference, _ in references if reference.startswith("#/") ) # start by taking all references use in /paths refs = refs_new = decompose_reference(get_elements(swagger, JSPATH_PATHS_REFERENCES)) while True: swagger_new = {section: {} for section in REFERENCE_SECTIONS} for rt, obj in refs_new: # handle only local references swagger_new[rt][obj] = swagger[rt][obj] refs_new = decompose_reference(get_elements(swagger_new, JSPATH_REFERENCES)) if refs_new.issubset(refs): break refs |= refs_new actions = [] for _, _, ref_path in get_elements(swagger, JSPATH_COMPONENTS): if ref_path not in refs: # the reference is not used, remove it rt, obj = ref_path del swagger[rt][obj] actions.append( ReferenceNotUsedFilterAction(path=(rt, obj), reason="reference not used") ) # remove sections that are left empty for section in REFERENCE_SECTIONS: if section in swagger and not swagger[section]: del swagger[section] return swagger, actions
def resolve_security(swagger): """Resolve security in swagger (in place) Apply the global security if defined to each operation when the latter has no security defined """ # resolve security at global level to operation level global_security = swagger.get("security") if global_security is not None: for key, value, path in get_elements(swagger, JSPATH_OPERATIONS): value.setdefault("security", global_security)
def check_schema(swagger: Dict) -> Set[ValidationError]: """Check swagger is compliant with schema""" # validate the json schema of the swagger_lib schema = json.load((Path(__file__).parent / "schemas" / "schema_swagger.json").open()) v = Draft4Validator(schema) # convert any key to string (as json swagger expects all keys to be str and response code are sometimes integer) for name, value, path in get_elements(swagger, JSPATH_OPERATION_RESPONSES): for k in list(value.keys()): if isinstance(k, int): value[str(k)] = value.pop(k) return { JsonSchemaValidationError(path=tuple(error.absolute_path), reason=error.message) for error in v.iter_errors(swagger) }
def prune_empty_paths(swagger): """Prune the swagger (in place) of its empty paths (ie paths with no verb)""" # list all operations (paths without any operation are not included actions = [] for endpoint_name, endpoint, path in get_elements(swagger, JSPATH_ENDPOINTS): if not endpoint or len(endpoint) == 1 and "parameters" in endpoint: # endpoint is empty, remove it del swagger["paths"][endpoint_name] actions.append( PathsEmptyFilterError( path=path, reason=f"path '{endpoint_name}' has no operations defined" ) ) return swagger, actions
def check_references(swagger: Dict): """ Find reference in paths, for /definitions/ and /responses/ /securityDefinitions/. Follow from these, references to other references, till no more added. :param swagger: :return: """ events = set() ref_jspath = JSPATH_REFERENCES for _, reference, path in get_elements(swagger, ref_jspath): # handle only local references if reference.startswith("#/"): # decompose reference (error if not possible) try: rt, obj = reference[2:].split("/") except ValueError: events.add( ReferenceInvalidSyntax( path=path, reason=f"reference {reference} not of the form '#/section/item'" ) ) continue if rt not in REFERENCE_SECTIONS: events.add( ReferenceInvalidSection( path=path, reason=f"Reference {reference} not referring to one of the sections {REFERENCE_SECTIONS}", ) ) # resolve reference (error if not possible) try: swagger[rt][obj] except KeyError: events.add( ReferenceNotFoundValidationError( path=path, reason=f"reference '#/{rt}/{obj}' does not exist" ) ) return events
def check_security(swagger: Dict): """ Check that uses of security with its scopes matches a securityDefinition :param swagger: :return: """ events = set() secdefs = swagger.get("securityDefinitions", {}) security_jspath = JSPATH_SECURITY for sec_key, scopes, path in get_elements(swagger, security_jspath): # retrieve security definition name from security declaration secdef = secdefs.get(sec_key) if secdef is None: events.add( SecurityDefinitionNotFoundValidationError( path=path, reason=f"securityDefinitions '{sec_key}' does not exist" ) ) else: # retrieve scopes declared in the secdef declared_scopes = secdef.get("scopes", []) if not isinstance(scopes, list): continue # verify scopes can be resolved for scope in scopes: if scope not in declared_scopes: events.add( OAuth2ScopeNotFoundInSecurityDefinitionValidationError( path=path + (scope,), reason=f"scope {scope} is not declared in the scopes of the securityDefinitions '{sec_key}'", ) ) return events
def prune_unused_security_definitions(swagger): """Prune the swagger (in place) of its unused securityDefinitions or oauth scopes""" if "securityDefinitions" not in swagger: return swagger, [] security_jspath = JSPATH_SECURITY # detect security definitions used and for which scope secdefs_used = defaultdict(set) for sec_name, sec_scopes, _ in get_elements(swagger, security_jspath): secdefs_used[sec_name].update(sec_scopes) # iterate existing securityDefinitions to check if they are used and if their scopes are used actions = [] for sec_name, sec_def in swagger["securityDefinitions"].copy().items(): if sec_name not in secdefs_used: del swagger["securityDefinitions"][sec_name] actions.append( SecurityDefinitionNotUsedFilterAction( path=("securityDefinitions", sec_name), reason="security definition not used" ) ) elif "scopes" in sec_def: for scope_name, scope_def in sec_def["scopes"].copy().items(): if scope_name not in secdefs_used[sec_name]: del swagger["securityDefinitions"][sec_name]["scopes"][scope_name] actions.append( OAuth2ScopeNotUsedFilterAction( path=("securityDefinitions", sec_name, "scopes", scope_name), reason="oauth2 scope not used", ) ) # remove securityDefinitions if empty if not swagger["securityDefinitions"]: del swagger["securityDefinitions"] return swagger, actions
def check_parameters(swagger: Dict): """ Check parameters for: - duplicate items in enum - default parameter is in line with type when type=string :param swagger: :return: """ events = set() parameters_jspath = JSPATH_PARAMETERS for _, param, path in get_elements(swagger, parameters_jspath): while True: events |= _check_parameter(param, path) if param.get("type") == "array": # recurse in array items type path += ("items",) param = param.get("items", {}) else: break return events
def filter(swagger: Dict, mode="keep_only", conditions: List[FilterCondition] = None ) -> Tuple[Dict, List[FilterAction]]: """ Filter endpoints of a swagger specification. The endpoints can be filtered according to two modes: - keep_only: it will keep only the operations matching any of the conditions - remove: it will remove only the operations matching any of the conditions (TO BE IMPLEMENTED) The conditions parameter is a list of FilterCondition objects containing each: - tags: the operation is kept only if it has at least one tag in the tags - operations: the operation is kept only if its VERB + PATH matches at least one operation in the operations - security_scopes: the operation is kept only if it requires no security or if some of its security items only requires the scopes in the security_scopes Any of these fields can be None to avoid matching on the field criteria. :param mode: :param conditions: :param swagger: the swagger spec :return: filtered swagger, a set of actions """ if mode != "keep_only": raise NotImplementedError(f"The mode '{mode}' is not yet implemented.") if conditions is None: return swagger, [] swagger = copy.deepcopy(swagger) global_security = swagger.get("security") filter = generate_filter_conditions(conditions, merge_matches=True, global_security=global_security) # if global security defined, filter it also if global_security is not None and filter.on_security_scopes_useful: match = filter((), swagger, on_tags=False, on_operations=False) if match: swagger = match else: # TODO: as the global security does not match with the conditions # we could already remove from the paths all operations with no # security defined (optimization trick) del swagger["security"] # get operations to keep operations_to_keep = { path: filter(path, operation) for key, operation, path in get_elements(swagger, JSPATH_OPERATIONS) } # update the paths actions = [] paths = swagger["paths"] for path, new_value in operations_to_keep.items(): (_, endpoint, verb) = path if new_value is not False: if paths[endpoint][verb] != new_value: actions.append( OperationChangedFilterAction( path=path, reason="The operation has been modified by a filter.")) paths[endpoint][verb] = new_value else: actions.append( OperationRemovedFilterAction( path=path, reason= "The operation has been removed as it does not match any filter.", )) del paths[endpoint][verb] return swagger, actions