Source code for dmr.openapi.generators.security_scheme

import dataclasses
import itertools
from typing import TYPE_CHECKING, ClassVar, TypeAlias

from dmr.semantic_schema import (
    OrSecurityRequirementMerger,
    SecurityProvider,
    SecurityRequirementMerger,
)

if TYPE_CHECKING:
    from dmr.controller import Controller
    from dmr.metadata import EndpointMetadata
    from dmr.openapi.core.context import OpenAPIContext
    from dmr.openapi.objects import (
        Reference,
        SecurityRequirement,
        SecurityScheme,
    )
    from dmr.serializer import BaseSerializer


_SecuritySchemes: TypeAlias = dict[str, 'SecurityScheme | Reference']


[docs] @dataclasses.dataclass(frozen=True, slots=True) class SecuritySchemeGenerator: # noqa: WPS214 """ Generator for OpenAPI Security Schemes. Responsible for processing authentication providers, extracting their security schemes, registering them in the context, and returning the corresponding security requirements for the operation. User provided ``security`` requirements are merged with the ones generated from ``auth`` by :attr:`security_merger`. Override it in a subclass and set the subclass as :attr:`dmr.openapi.OpenAPIContext.security_scheme_cls` to change how they are merged. .. versionchanged:: 0.16.0 Added :attr:`security_merger` and ``security`` support. """ #: Merges user provided ``security`` with requirements from ``auth``. #: By default, they are joined as alternatives with ``OR`` logic. security_merger: ClassVar[SecurityRequirementMerger] = ( OrSecurityRequirementMerger() ) _context: 'OpenAPIContext'
[docs] def __call__( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], ) -> list['SecurityRequirement'] | None: """ Process auth providers and generate security requirements. Iterates over the provided authentication providers, registers their security schemes in the global registry, and collects their security usage requirements. Semantic schema providers can then inject their own requirements into the collected ones. User provided ``security`` from *metadata* is merged last with :attr:`security_merger`. Its security schemes are never registered, they must be declared in ``components`` of :class:`~dmr.openapi.OpenAPIConfig`. When there are no requirements but the document defines global ``security``, returns an explicit ``[]`` so the operation opts out of the global requirements instead of inheriting them. .. versionchanged:: 0.16.0 Now accepts *metadata* and *controller_cls* parameters. User provided ``security`` requirements are merged as well. """ # How it works? # # First, of all: we have security schemes and security requirements. # Secondly: we have several different ways of how they can be provided. # # Security schemes # ---------------- # Can be provided via: `metadata.auth` classes, # `OpenAPIConfig.components.security_schemes` field, # semantic schema providers. # We always register `metadata.auth` schemes as-is. # `security_schemes` is applied in config merger, this is also # where schemes used in `metadata.security` must be declared. # We only register security schemes from semantic schema providers, # that are actually used. # # Security requirements # --------------------- # Can be provided via: `metadata.auth`, `metadata.security`, # `OpenAPIConfig.security`, semantic schema providers. # We always use `metadata.auth` as-is. # We process all existing `metadata.auth` security requirements # to possibly inject extra ones from semantic schemas. # `metadata.security` is merged last with `security_merger`, # by default it is added as-is as alternatives to the auth ones. # `OpenAPIConfig.security` is the settings level of `metadata.security`, # it is also applied to the whole document in config merger. self._register_auth_security_schemes(metadata, controller_cls) requirements = self._prepare_requirements(metadata, controller_cls) requirements, semantic_schemes = self._inject_semantic_schema( metadata, controller_cls, requirements, ) self._register_security_schemes( metadata, controller_cls, semantic_schemes, ) requirements = self._merge_user_security( metadata, controller_cls, requirements, ) # Finally, return the result: if not requirements: # If global security is set, # but this endpoint does not have any auth, # it must return explicit `[]`, so it's auth would be re-written: return ( [] if ( isinstance(self._context.config.security, list) and self._context.config.security ) else None ) return requirements
def _register_auth_security_schemes( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], ) -> None: for auth in metadata.auth or []: self._register_security_schemes( metadata, controller_cls, auth.security_schemes( metadata, controller_cls, ), ) def _register_security_schemes( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], security_schemes: _SecuritySchemes, ) -> None: for scheme_name, scheme in security_schemes.items(): if ( not metadata.semantic_auth or scheme_name in metadata.exclude_semantic_auth ): continue self._context.registries.security_scheme.register( scheme_name, scheme, ) def _prepare_requirements( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], ) -> list['SecurityRequirement']: requirements: list[SecurityRequirement] = [] for auth in metadata.auth or []: requirements.extend( new_requirement for requirement in auth.security_requirements( metadata, controller_cls, ) if ( new_requirement := _filter_requirement( requirement, metadata, ) ) is not None ) return requirements def _inject_semantic_schema( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], requirements: list['SecurityRequirement'], ) -> tuple[ list['SecurityRequirement'], _SecuritySchemes, ]: semantic_providers = self._resolve_auth_semantic_providers( metadata, controller_cls, ) new_requirements: list[SecurityRequirement] = [] for provider in semantic_providers: semantic_requirements = provider.security_requirements( metadata, controller_cls, ) new_requirements.extend( # Custom SecurityProvider in the semantic schema # has a priority over the default merger: provider.merge_security_requirements( metadata, controller_cls, semantic_requirements, requirements, ) if isinstance(provider, SecurityRequirementMerger) else self.security_merger.merge_security_requirements( metadata, controller_cls, semantic_requirements, requirements, ), ) return new_requirements, self._semantic_security_schemes( metadata, controller_cls, semantic_providers, new_requirements, ) def _merge_user_security( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], auth_requirements: list['SecurityRequirement'], ) -> list['SecurityRequirement']: if metadata.security is None: return auth_requirements return self.security_merger.merge_security_requirements( metadata, controller_cls, list(metadata.security), auth_requirements, ) def _semantic_security_schemes( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], semantic_providers: list[SecurityProvider], requirements: list['SecurityRequirement'], ) -> dict[str, 'SecurityScheme | Reference']: used_requirements = frozenset( itertools.chain.from_iterable(req.keys() for req in requirements), ) schemes = [ provider.security_schemes(metadata, controller_cls) for provider in semantic_providers ] used_schemes: dict[str, SecurityScheme | Reference] = {} for scheme in schemes: used_schemes.update({ scheme_name: scheme[scheme_name] for scheme_name in scheme if scheme_name in used_requirements }) return used_schemes def _resolve_auth_semantic_providers( self, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], ) -> list[SecurityProvider]: from dmr.settings import Settings, resolve_setting # noqa: PLC0415 return [ provider for provider in resolve_setting(Settings.semantic_schema_providers) if isinstance(provider, SecurityProvider) ]
def _filter_requirement( requirement: 'SecurityRequirement', metadata: 'EndpointMetadata', ) -> 'SecurityRequirement | None': return { req_name: req_value for req_name, req_value in requirement.items() if ( metadata.semantic_auth and req_name not in metadata.exclude_semantic_auth ) } or None