Example #1
0
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
Example #2
0
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
Example #3
0
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
Example #4
0
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)
Example #5
0
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)
    }
Example #6
0
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
Example #7
0
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
Example #8
0
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
Example #9
0
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
Example #10
0
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
Example #11
0
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