authentik.api.v3.schema.cleanup

 1"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
 2
 3from collections.abc import Callable
 4from typing import Any
 5
 6from drf_spectacular.contrib.django_filters import (
 7    DjangoFilterExtension as BaseDjangoFilterExtension,
 8)
 9from drf_spectacular.generators import SchemaGenerator
10from drf_spectacular.plumbing import (
11    ResolvedComponent,
12    follow_field_source,
13)
14from drf_spectacular.renderers import OpenApiJsonRenderer
15from drf_spectacular.settings import spectacular_settings
16from structlog.stdlib import get_logger
17
18from authentik.api.apps import AuthentikAPIConfig
19
20LOGGER = get_logger()
21
22
23def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
24    """Filter out all API Views which are not mounted under /api"""
25    return [
26        (path, path_regex, method, callback)
27        for path, path_regex, method, callback in endpoints
28        if path.startswith("/" + AuthentikAPIConfig.mountpoint)
29    ]
30
31
32def postprocess_schema_remove_unused(
33    result: dict[str, Any], generator: SchemaGenerator, **kwargs
34) -> dict[str, Any]:
35    """Remove unused components"""
36    # To check if the schema is used, render it to JSON and then substring check that
37    # less efficient than walking through the tree but a lot simpler and no
38    # possibility that we miss something
39    raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
40    count = 0
41    for key in result["components"][ResolvedComponent.SCHEMA].keys():
42        schema_usages = raw.count(f"#/components/{ResolvedComponent.SCHEMA}/{key}")
43        if schema_usages >= 1:
44            continue
45        del generator.registry[(key, ResolvedComponent.SCHEMA)]
46        count += 1
47    LOGGER.debug("Removing unused components", count=count)
48    result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
49    return result
50
51
52class DjangoFilterExtension(BaseDjangoFilterExtension):
53    """
54    From https://github.com/netbox-community/netbox/pull/21521:
55
56    Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where
57    _get_model_field() incorrectly double-appends to_field_name when field_name already ends
58    with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid
59    path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema
60    generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.
61
62    See: https://github.com/netbox-community/netbox/issues/20787
63         https://github.com/tfranzel/drf-spectacular/issues/1475
64    """
65
66    priority = 1
67
68    def _get_model_field(self, filter_field, model):
69        if not filter_field.field_name:
70            return None
71        path = filter_field.field_name.split("__")
72        to_field_name = filter_field.extra.get("to_field_name")
73        if to_field_name is not None and path[-1] != to_field_name:
74            path.append(to_field_name)
75        return follow_field_source(model, path, emit_warnings=False)
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
def preprocess_schema_exclude_non_api( endpoints: list[tuple[str, typing.Any, typing.Any, Callable]], **kwargs):
24def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
25    """Filter out all API Views which are not mounted under /api"""
26    return [
27        (path, path_regex, method, callback)
28        for path, path_regex, method, callback in endpoints
29        if path.startswith("/" + AuthentikAPIConfig.mountpoint)
30    ]

Filter out all API Views which are not mounted under /api

def postprocess_schema_remove_unused( result: dict[str, typing.Any], generator: drf_spectacular.generators.SchemaGenerator, **kwargs) -> dict[str, typing.Any]:
33def postprocess_schema_remove_unused(
34    result: dict[str, Any], generator: SchemaGenerator, **kwargs
35) -> dict[str, Any]:
36    """Remove unused components"""
37    # To check if the schema is used, render it to JSON and then substring check that
38    # less efficient than walking through the tree but a lot simpler and no
39    # possibility that we miss something
40    raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
41    count = 0
42    for key in result["components"][ResolvedComponent.SCHEMA].keys():
43        schema_usages = raw.count(f"#/components/{ResolvedComponent.SCHEMA}/{key}")
44        if schema_usages >= 1:
45            continue
46        del generator.registry[(key, ResolvedComponent.SCHEMA)]
47        count += 1
48    LOGGER.debug("Removing unused components", count=count)
49    result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
50    return result

Remove unused components

class DjangoFilterExtension(drf_spectacular.plumbing.OpenApiGeneratorExtension[ForwardRef('OpenApiFilterExtension')]):
53class DjangoFilterExtension(BaseDjangoFilterExtension):
54    """
55    From https://github.com/netbox-community/netbox/pull/21521:
56
57    Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where
58    _get_model_field() incorrectly double-appends to_field_name when field_name already ends
59    with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid
60    path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema
61    generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.
62
63    See: https://github.com/netbox-community/netbox/issues/20787
64         https://github.com/tfranzel/drf-spectacular/issues/1475
65    """
66
67    priority = 1
68
69    def _get_model_field(self, filter_field, model):
70        if not filter_field.field_name:
71            return None
72        path = filter_field.field_name.split("__")
73        to_field_name = filter_field.extra.get("to_field_name")
74        if to_field_name is not None and path[-1] != to_field_name:
75            path.append(to_field_name)
76        return follow_field_source(model, path, emit_warnings=False)

From https://github.com/netbox-community/netbox/pull/21521:

Overrides drf-spectacular's DjangoFilterExtension to fix a regression in v0.29.0 where _get_model_field() incorrectly double-appends to_field_name when field_name already ends with that value (e.g. field_name='tags__slug', to_field_name='slug' produces the invalid path ['tags', 'slug', 'slug']). This caused hundreds of spurious warnings during schema generation for filters such as TagFilter, TenancyFilterSet.tenant, and OwnerFilterMixin.owner.

See: https://github.com/netbox-community/netbox/issues/20787 https://github.com/tfranzel/drf-spectacular/issues/1475

priority = 1