-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Raphael Krupinski
committed
Jan 9, 2024
1 parent
26c6999
commit 1940bfd
Showing
21 changed files
with
331 additions
and
159 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
# Authentication | ||
|
||
OpenAPI allows declaring security schemes and security requirements of operations. | ||
|
||
Lapidary allows to declare python methods that create or consume `https.Auth` objects. | ||
|
||
## Generating auth tokens | ||
|
||
|
||
A `/login/` or `/authenticate/` endpoint that returns the token is quite common with simpler authentication schemes like http or apiKey, yet their support is poor in OpenAPI. There's no way to connect | ||
such endpoint to a security scheme as in the case of OIDC. | ||
|
||
A function that handles such an endpoint can declare that it returns an Auth object, but it's not obvious to the user of python API which security scheme the method returns. | ||
|
||
```python | ||
import pydantic | ||
import typing_extensions as ty | ||
from lapidary.runtime import POST, ClientBase, RequestBody, APIKeyAuth, Responses | ||
from httpx import Auth | ||
|
||
|
||
class LoginRequest(pydantic.BaseModel): | ||
... | ||
|
||
|
||
class LoginResponse(pydantic.BaseModel): | ||
token: str | ||
|
||
|
||
class Client(ClientBase): | ||
@POST('/login') | ||
def login( | ||
self: ty.Self, | ||
*, | ||
body: ty.Annotated[LoginRequest, RequestBody({'application/json': LoginRequest})], | ||
) -> ty.Annotated[ | ||
Auth, | ||
Responses({ | ||
'200': { | ||
'application/json': ty.Annotated[ | ||
LoginResponse, | ||
APIKeyAuth( | ||
in_='header', | ||
name='Authorization', | ||
format='Token {body.token}' | ||
), | ||
] | ||
} | ||
}), | ||
]: | ||
"""Authenticates with the "primary" security scheme""" | ||
``` | ||
|
||
The top return Annotated declares the returned type, the inner one declares the processing steps for the actual response. | ||
First the response is parsed as LoginResponse, then that object is passed to ApiKeyAuth which is a callable object. | ||
|
||
The result of the call, in this case an Auth object, is returned by the `login` function. | ||
|
||
The innermost Annotated is not necessary from the python syntax standpoint. It's done this way since it kind of matches the semantics of Annotated, but it could be replaced with a simple tuple or other type in the future. | ||
|
||
## Using auth tokens | ||
|
||
OpenApi allows operations to declare a collection of alternative groups of security requirements. | ||
|
||
The second most trivial example (the first being no security) is a single required security scheme. | ||
```yaml | ||
security: | ||
- primary: [] | ||
``` | ||
The name of the security scheme corresponds to a key in `components.securitySchemes` object. | ||
|
||
This can be represented as a simple parameter, named for example `primary_auth` and of type `httpx.Auth`. | ||
The parameter could be annotated as `Optional` if the security requirement is optional for the operation. | ||
|
||
In case of multiple alternative groups of security requirements, it gets harder to properly describe which schemes are required and in what combination. | ||
|
||
Lapidary takes all passed `httpx.Auth` parameters and passes them to `httpx.AsyncClient.send(..., auth=auth_params)`, leaving the responsibility to select the right ones to the user. | ||
|
||
If multiple `Auth` parameters are passed, they're wrapped in `lapidary.runtime.aauth.MultiAuth`, which is just reexported `_MultiAuth` from `https_auth` package. | ||
|
||
#### Example | ||
|
||
Auth object returned by the login operation declared in the above example can be used by another operation. | ||
|
||
```python | ||
from typing import Annotated, Self | ||
from httpx import Auth | ||
from lapidary.runtime import ClientBase, GET, POST | ||
class Client(ClientBase): | ||
@POST('/login') | ||
def login( | ||
self: Self, | ||
body: ..., | ||
) -> Annotated[ | ||
Auth, | ||
... | ||
]: | ||
"""Authenticates with the "primary" security scheme""" | ||
@GET('/private') | ||
def private( | ||
self: Self, | ||
*, | ||
primary_auth: Auth, | ||
): | ||
pass | ||
``` | ||
|
||
In this example the method `client.private` can be called with the auth object returned by `client.login`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,7 +64,7 @@ TBD | |
|
||
## Auth | ||
|
||
TBD | ||
See [Auth](auth.md). | ||
|
||
## Servers | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
from .absent import ABSENT, Absent | ||
from .auth import APIKeyAuth | ||
from .client_base import ClientBase | ||
from .model.params import ParamStyle | ||
from .model.params import ParamLocation, ParamStyle | ||
from .model.request import RequestBody | ||
from .model.response_map import Responses | ||
from .operation import DELETE, GET, HEAD, PATCH, POST, PUT, TRACE | ||
from .param import Cookie, Header, Path, Query |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
__all__ = [ | ||
'APIKeyAuth', | ||
'MultiAuth', | ||
] | ||
|
||
import abc | ||
import dataclasses as dc | ||
from typing import Mapping | ||
import typing as ty | ||
|
||
import httpx | ||
import httpx_auth | ||
from httpx_auth.authentication import _MultiAuth as MultiAuth, _MultiAuth | ||
|
||
from .compat import typing as ty | ||
from .model.api_key import CookieApiKey | ||
|
||
AuthType = ty.TypeVar("AuthType", bound=httpx.Auth) | ||
|
||
|
||
class AuthFactory(abc.ABC): | ||
@abc.abstractmethod | ||
def __call__(self, body: object) -> httpx.Auth: | ||
pass | ||
|
||
|
||
APIKeyAuthLocation: ty.TypeAlias = ty.Literal['cookie', 'header', 'query'] | ||
|
||
api_key_in: ty.Mapping[APIKeyAuthLocation, ty.Type[AuthType]] = { | ||
'cookie': CookieApiKey, | ||
'header': httpx_auth.authentication.HeaderApiKey, | ||
'query': httpx_auth.authentication.QueryApiKey, | ||
} | ||
|
||
|
||
@dc.dataclass | ||
class APIKeyAuth(AuthFactory): | ||
in_: APIKeyAuthLocation | ||
name: str | ||
format: str | ||
|
||
def __call__(self, body: object) -> httpx.Auth: | ||
typ = api_key_in[self.in_] | ||
return typ(self.format.format(body=body), self.name) | ||
|
||
|
||
def get_auth(params: Mapping[str, ty.Any]) -> ty.Optional[httpx.Auth]: | ||
auth_params = [value for value in params.values() if isinstance(value, httpx.Auth)] | ||
auth_num = len(auth_params) | ||
if auth_num == 0: | ||
return None | ||
elif auth_num == 1: | ||
return auth_params[0] | ||
else: | ||
return MultiAuth(*auth_params) |
Empty file.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +0,0 @@ | ||
from .params import FullParam, ParamLocation | ||
from .response_map import ResponseMap, ReturnTypeInfo | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import httpx | ||
import httpx_auth.authentication as authx | ||
|
||
from ..compat import typing as ty | ||
|
||
|
||
class CookieApiKey(httpx.Auth, authx.SupportMultiAuth): | ||
"""Describes an API Key requests authentication.""" | ||
|
||
def __init__(self, api_key: str, cookie_name: str = None): | ||
""" | ||
:param api_key: The API key that will be sent. | ||
:param cookie_name: Name of the query parameter. "api_key" by default. | ||
""" | ||
self.api_key = api_key | ||
if not api_key: | ||
raise Exception("API Key is mandatory.") | ||
self.cookie_parameter_name = cookie_name or "api_key" | ||
|
||
def auth_flow( | ||
self, request: httpx.Request | ||
) -> ty.Generator[httpx.Request, httpx.Response, None]: | ||
request.headers['Cookie'] = f'{self.cookie_parameter_name}={self.api_key}' | ||
yield request |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.