From 54b5a9791b5b9a64efbf01bfc8d53b261e31f00b Mon Sep 17 00:00:00 2001 From: tarsil Date: Thu, 5 Oct 2023 19:22:34 +0100 Subject: [PATCH] Add security schemes to openapi --- esmerald/contrib/auth/common/middleware.py | 2 +- esmerald/openapi/enums.py | 25 ++++--- esmerald/openapi/openapi.py | 3 +- esmerald/openapi/security/api_key/__init__.py | 7 ++ esmerald/openapi/security/api_key/base.py | 73 ++++++++++++++++++ esmerald/openapi/security/base.py | 43 +++++++++++ esmerald/openapi/security/http/__init__.py | 11 ++- esmerald/openapi/security/http/base.py | 74 +++++++++++++++---- esmerald/openapi/security/oauth2/__init__.py | 5 ++ esmerald/openapi/security/oauth2/base.py | 49 ++++++++++++ .../security/openid_connect/__init__.py | 3 + .../openapi/security/openid_connect/base.py | 27 +++++++ 12 files changed, 289 insertions(+), 33 deletions(-) create mode 100644 esmerald/openapi/security/api_key/__init__.py create mode 100644 esmerald/openapi/security/api_key/base.py create mode 100644 esmerald/openapi/security/base.py create mode 100644 esmerald/openapi/security/oauth2/__init__.py create mode 100644 esmerald/openapi/security/oauth2/base.py create mode 100644 esmerald/openapi/security/openid_connect/__init__.py create mode 100644 esmerald/openapi/security/openid_connect/base.py diff --git a/esmerald/contrib/auth/common/middleware.py b/esmerald/contrib/auth/common/middleware.py index f9501f95..4116d879 100644 --- a/esmerald/contrib/auth/common/middleware.py +++ b/esmerald/contrib/auth/common/middleware.py @@ -74,7 +74,7 @@ async def authenticate(self, request: HTTPConnection) -> AuthResult: auth_token = token_partition[-1] if token_type not in self.config.auth_header_types: - raise NotAuthorized(detail=f"{token_type} is not an authorized header type") + raise NotAuthorized(detail=f"'{token_type}' is not an authorized header.") try: token = Token.decode( diff --git a/esmerald/openapi/enums.py b/esmerald/openapi/enums.py index 4bee7119..2e88e887 100644 --- a/esmerald/openapi/enums.py +++ b/esmerald/openapi/enums.py @@ -1,26 +1,27 @@ from enum import Enum -class SecuritySchemeType(str, Enum): - apiKey = "apiKey" - http = "http" - oauth2 = "oauth2" - openIdConnect = "openIdConnect" - +class BaseEnum(str, Enum): def __str__(self) -> str: - return self.value + return self.value # type: ignore def __repr__(self) -> str: return str(self) -class APIKeyIn(str, Enum): +class SecuritySchemeType(BaseEnum): + apiKey = "apiKey" + http = "http" + oauth2 = "oauth2" + mutualTLS = "mutualTLS" + openIdConnect = "openIdConnect" + + +class APIKeyIn(BaseEnum): query = "query" header = "header" cookie = "cookie" - def __str__(self) -> str: - return self.value - def __repr__(self) -> str: - return str(self) +class Header(BaseEnum): + authorization = "Authorization" diff --git a/esmerald/openapi/openapi.py b/esmerald/openapi/openapi.py index 2ca4840c..a7bdc901 100644 --- a/esmerald/openapi/openapi.py +++ b/esmerald/openapi/openapi.py @@ -54,7 +54,7 @@ def get_flat_params(route: Union[router.HTTPHandler, Any]) -> List[Any]: def get_openapi_security_schemes(schemes: Any) -> Tuple[dict, list]: """ - Builds the security schemes.di + Builds the security schemas for OpenAPI. """ security_definitions = {} operation_security = [] @@ -71,6 +71,7 @@ def get_openapi_security_schemes(schemes: Any) -> Tuple[dict, list]: security_name = security_requirement.scheme_name security_definitions[security_name] = security_definition operation_security.append({security_name: security_requirement}) + return security_definitions, operation_security diff --git a/esmerald/openapi/security/api_key/__init__.py b/esmerald/openapi/security/api_key/__init__.py new file mode 100644 index 00000000..9933988e --- /dev/null +++ b/esmerald/openapi/security/api_key/__init__.py @@ -0,0 +1,7 @@ +from .base import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery + +__all__ = [ + "APIKeyInCookie", + "APIKeyInHeader", + "APIKeyInQuery", +] diff --git a/esmerald/openapi/security/api_key/base.py b/esmerald/openapi/security/api_key/base.py new file mode 100644 index 00000000..cdfecdab --- /dev/null +++ b/esmerald/openapi/security/api_key/base.py @@ -0,0 +1,73 @@ +from typing import Any, Literal, Optional + +from esmerald.openapi.enums import APIKeyIn, SecuritySchemeType +from esmerald.openapi.security.base import HTTPBase + + +class APIKeyInQuery(HTTPBase): + def __init__( + self, + *, + type_: Literal[ + "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" + ] = SecuritySchemeType.apiKey.value, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.query.value, + name: Optional[str] = None, + **kwargs: Any, + ): + super().__init__( + type_=type_, + description=description, + name=name, + in_=in_, + scheme_name=scheme_name or self.__class__.__name__, + **kwargs, + ) + + +class APIKeyInHeader(HTTPBase): + def __init__( + self, + *, + type_: Literal[ + "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" + ] = SecuritySchemeType.apiKey.value, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.header.value, + name: Optional[str] = None, + **kwargs: Any, + ): + super().__init__( + type_=type_, + description=description, + name=name, + in_=in_, + scheme_name=scheme_name or self.__class__.__name__, + **kwargs, + ) + + +class APIKeyInCookie(HTTPBase): + def __init__( + self, + *, + type_: Literal[ + "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" + ] = SecuritySchemeType.apiKey.value, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.cookie.value, + name: Optional[str] = None, + **kwargs: Any, + ): + super().__init__( + type_=type_, + description=description, + name=name, + in_=in_, + scheme_name=scheme_name or self.__class__.__name__, + **kwargs, + ) diff --git a/esmerald/openapi/security/base.py b/esmerald/openapi/security/base.py new file mode 100644 index 00000000..acebc609 --- /dev/null +++ b/esmerald/openapi/security/base.py @@ -0,0 +1,43 @@ +from typing import Any, Literal, Optional, Union + +from pydantic import AnyUrl, BaseModel, ConfigDict + +from esmerald.openapi.models import SecurityScheme + + +class HTTPAuthorizationCredentials(BaseModel): + scheme: str + credentials: str + + +class HTTPBase(SecurityScheme): + """ + Base for all HTTP security headers. + """ + + model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True) + + def __init__( + self, + *, + type_: Optional[Literal["apiKey", "http", "mutualTLS", "oauth2", "openIdConnect"]] = None, + bearerFormat: Optional[str] = None, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + in_: Optional[Literal["query", "header", "cookie"]] = None, + name: Optional[str] = None, + scheme: Optional[str] = None, + openIdConnectUrl: Optional[Union[AnyUrl, str]] = None, + **kwargs: Any, + ): + super().__init__( # type: ignore + type=type_, + bearerFormat=bearerFormat, + description=description, + name=name, + security_scheme_in=in_, + scheme_name=scheme_name, + scheme=scheme, + openIdConnectUrl=openIdConnectUrl, + **kwargs, + ) diff --git a/esmerald/openapi/security/http/__init__.py b/esmerald/openapi/security/http/__init__.py index e4ce115a..63fb6404 100644 --- a/esmerald/openapi/security/http/__init__.py +++ b/esmerald/openapi/security/http/__init__.py @@ -1,4 +1,7 @@ -# from .base import HTTPBasic as HTTPBasic -# from .base import HTTPAuthorizationCredentials as HTTPAuthorizationCredentials -# from .base import HTTPBasicCredentials as HTTPBasicCredentials -from .base import Bearer as Bearer +from .base import Basic, Bearer, Digest + +__all__ = [ + "Basic", + "Bearer", + "Digest", +] diff --git a/esmerald/openapi/security/http/base.py b/esmerald/openapi/security/http/base.py index 86ddf122..69de655a 100644 --- a/esmerald/openapi/security/http/base.py +++ b/esmerald/openapi/security/http/base.py @@ -1,19 +1,64 @@ from typing import Any, Literal, Optional -from pydantic import BaseModel, ConfigDict +from esmerald.openapi.enums import APIKeyIn, Header, SecuritySchemeType +from esmerald.openapi.security.base import HTTPBase -from esmerald.openapi.enums import APIKeyIn, SecuritySchemeType -from esmerald.openapi.models import SecurityScheme +class Basic(HTTPBase): + def __init__( + self, + *, + type_: Literal[ + "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" + ] = SecuritySchemeType.http.value, + bearerFormat: Optional[str] = None, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.header.value, + name: Optional[str] = None, + scheme: Optional[str] = None, + **kwargs: Any, + ): + super().__init__( + type_=type_, + bearerFormat=bearerFormat, + description=description, + name=name or "Basic", + in_=in_, + scheme=scheme or "basic", + scheme_name=scheme_name or self.__class__.__name__, + **kwargs, + ) -class HTTPAuthorizationCredentials(BaseModel): - scheme: str - credentials: str +class Bearer(HTTPBase): + def __init__( + self, + *, + type_: Literal[ + "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" + ] = SecuritySchemeType.http.value, + bearerFormat: Optional[str] = None, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + in_: Optional[Literal["query", "header", "cookie"]] = APIKeyIn.header.value, + name: Optional[str] = None, + scheme: Optional[str] = None, + **kwargs: Any, + ): + super().__init__( + type_=type_, + bearerFormat=bearerFormat, + description=description, + name=name or Header.authorization, + in_=in_, + scheme=scheme or "bearer", + scheme_name=scheme_name or self.__class__.__name__, + **kwargs, + ) -class Bearer(SecurityScheme): - model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True) +class Digest(HTTPBase): def __init__( self, *, @@ -29,13 +74,12 @@ def __init__( **kwargs: Any, ): super().__init__( - type=type_, - description=description, + type_=type_, bearerFormat=bearerFormat, - name=name, + description=description, + name=name or Header.authorization, + in_=in_, + scheme=scheme or "digest", + scheme_name=scheme_name or self.__class__.__name__, **kwargs, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.security_scheme_in = in_ - self.name = name or "Authorization" - self.scheme = scheme or "bearer" diff --git a/esmerald/openapi/security/oauth2/__init__.py b/esmerald/openapi/security/oauth2/__init__.py new file mode 100644 index 00000000..0d892e88 --- /dev/null +++ b/esmerald/openapi/security/oauth2/__init__.py @@ -0,0 +1,5 @@ +from .base import OAuth2 + +__all__ = [ + "OAuth2", +] diff --git a/esmerald/openapi/security/oauth2/base.py b/esmerald/openapi/security/oauth2/base.py new file mode 100644 index 00000000..ad4ccaf3 --- /dev/null +++ b/esmerald/openapi/security/oauth2/base.py @@ -0,0 +1,49 @@ +from typing import Any, Dict, Literal, Optional, Union + +from esmerald.openapi.enums import SecuritySchemeType +from esmerald.openapi.models import OAuthFlows +from esmerald.openapi.security.base import HTTPBase + + +class OAuth2(HTTPBase): + """ + The OAuth2 scheme. + + For every parameter of the OAuthFlows, expects a OAuthFlow object type. + + Example: + implicit: Optional[OAuthFlow] = OAuthFlow() + password: Optional[OAuthFlow] = OAuthFlow() + clientCredentials: Optional[OAuthFlow] = OAuthFlow() + authorizationCode: Optional[OAuthFlow] = OAuthFlow() + + flows: OAuthFlows( + implicit=implicit, + password=password, + clientCredentials=clientCredentials, + authorizationCode=authorizationCode, + ) + """ + + def __init__( + self, + *, + type_: Literal[ + "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" + ] = SecuritySchemeType.oauth2.value, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + name: Optional[str] = None, + flows: Union[OAuthFlows, Dict[str, Dict[str, Any]]] = OAuthFlows(), + **kwargs: Any, + ): + extra: Dict[Any, Any] = {} + extra["flows"] = flows + extra.update(kwargs) + super().__init__( + type_=type_, + description=description, + name=name, + scheme_name=scheme_name or self.__class__.__name__, + **extra, + ) diff --git a/esmerald/openapi/security/openid_connect/__init__.py b/esmerald/openapi/security/openid_connect/__init__.py new file mode 100644 index 00000000..aa15e1f3 --- /dev/null +++ b/esmerald/openapi/security/openid_connect/__init__.py @@ -0,0 +1,3 @@ +from .base import OpenIdConnect + +__all__ = ["OpenIdConnect"] diff --git a/esmerald/openapi/security/openid_connect/base.py b/esmerald/openapi/security/openid_connect/base.py new file mode 100644 index 00000000..706976cc --- /dev/null +++ b/esmerald/openapi/security/openid_connect/base.py @@ -0,0 +1,27 @@ +from typing import Any, Literal, Optional, Union + +from pydantic import AnyUrl + +from esmerald.openapi.enums import SecuritySchemeType +from esmerald.openapi.security.base import HTTPBase + + +class OpenIdConnect(HTTPBase): + def __init__( + self, + *, + type_: Literal[ + "apiKey", "http", "mutualTLS", "oauth2", "openIdConnect" + ] = SecuritySchemeType.openIdConnect.value, + openIdConnectUrl: Optional[Union[AnyUrl, str]] = None, + scheme_name: Optional[str] = None, + description: Optional[str] = None, + **kwargs: Any, + ): + super().__init__( + type_=type_, + description=description, + scheme_name=scheme_name or self.__class__.__name__, + openIdConnectUrl=openIdConnectUrl, + **kwargs, + )