kiba-core 0.5.3.dev47__tar.gz → 0.5.3.dev49__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/Dockerfile +0 -1
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/PKG-INFO +1 -1
- kiba_core-0.5.3.dev49/core/api/authorizer.py +134 -0
- kiba_core-0.5.3.dev49/core/api/middleware/database_connection_middleware.py +21 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/SOURCES.txt +1 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/pyproject.toml +1 -1
- kiba_core-0.5.3.dev49/tests/api/test_authorizer.py +265 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/uv.lock +704 -686
- kiba_core-0.5.3.dev47/core/api/authorizer.py +0 -50
- kiba_core-0.5.3.dev47/core/api/middleware/database_connection_middleware.py +0 -24
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/pull_request_template.md +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/workflows/deploy.yml +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/workflows/pull-request.yml +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/workflows/release.yml +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.gitignore +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/CHANGELOG.md +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/README.md +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/api_request.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/api_response.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/default_routes.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/health.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/json_route.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/logging_middleware.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/server_headers_middleware.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/aws_requester.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/cache.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/dict_cache.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/file_cache.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/exceptions.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/basic_authentication.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/jwt.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/rest_method.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/logging.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/discord_client.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/notification_client.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/slack_client.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/py.typed +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/aqs.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/message_queue.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/model.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/sqs.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/requester/requester.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/s3_manager.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/service_client.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/database.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/retriever.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/saver.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/async_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/chain_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/date_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/dict_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/file_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/hashing_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/http_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/json_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/list_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/string_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/typing_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/url_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/value_holder.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/web3/eth_client.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/web3/multicall3.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/dependency_links.txt +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/requires.txt +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/top_level.txt +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/makefile +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/setup.cfg +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/api/test_json_route.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/api/test_streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/caching/test_dict_cache.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/caching/test_file_cache.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_async_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_chain_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_date_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_dict_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_file_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_json_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_list_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_string_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_url_util.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/web3/test_rest_eth_client.py +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import typing
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from typing import ParamSpec
|
|
5
|
+
|
|
6
|
+
from mypy_extensions import Arg
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from core import logging
|
|
10
|
+
from core.api.api_request import KibaApiRequest
|
|
11
|
+
from core.exceptions import ForbiddenException
|
|
12
|
+
from core.exceptions import UnauthorizedException
|
|
13
|
+
from core.http.basic_authentication import BasicAuthentication
|
|
14
|
+
from core.http.jwt import Jwt
|
|
15
|
+
|
|
16
|
+
_P = ParamSpec('_P')
|
|
17
|
+
_AnyReturn = typing.Awaitable[typing.Any] | AsyncIterator[typing.Any] # type: ignore[explicit-any]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Authorizer:
|
|
21
|
+
async def validate_jwt(self, jwtString: str) -> Jwt:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SignatureAuthorizer:
|
|
26
|
+
async def retrieve_signature_signer(self, signatureString: str) -> str:
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _authorize_bearer_jwt[ApiRequest: BaseModel](request: KibaApiRequest[ApiRequest], authorizer: Authorizer) -> Jwt:
|
|
31
|
+
authorization = request.headers.get('Authorization')
|
|
32
|
+
if not authorization:
|
|
33
|
+
raise ForbiddenException(message='AUTH_NOT_PROVIDED')
|
|
34
|
+
if not authorization.startswith('Bearer '):
|
|
35
|
+
raise ForbiddenException(message='AUTH_INVALID')
|
|
36
|
+
jwtString = authorization.replace('Bearer ', '')
|
|
37
|
+
try:
|
|
38
|
+
jwt = await authorizer.validate_jwt(jwtString=jwtString)
|
|
39
|
+
except BaseException: # noqa: BLE001
|
|
40
|
+
raise ForbiddenException(message='AUTH_INVALID')
|
|
41
|
+
return jwt
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def authorize_bearer_jwt[ApiRequest: BaseModel]( # type: ignore[explicit-any]
|
|
45
|
+
authorizer: Authorizer,
|
|
46
|
+
) -> typing.Callable[[typing.Callable[[Arg(KibaApiRequest[ApiRequest], 'request')], _AnyReturn]], typing.Callable[_P, typing.Any]]:
|
|
47
|
+
def decorator(func: typing.Callable[[Arg(KibaApiRequest[ApiRequest], 'request')], _AnyReturn]) -> typing.Callable[_P, typing.Any]: # type: ignore[explicit-any]
|
|
48
|
+
@functools.wraps(func)
|
|
49
|
+
async def async_wrapper(request: KibaApiRequest[ApiRequest]) -> typing.Any: # type: ignore[explicit-any, misc]
|
|
50
|
+
request.authJwt = await _authorize_bearer_jwt(request=request, authorizer=authorizer)
|
|
51
|
+
result = func(request=request)
|
|
52
|
+
# NOTE(krishan711): this is here to support streaming responses which return an async generator
|
|
53
|
+
if hasattr(result, '__aiter__'):
|
|
54
|
+
return result
|
|
55
|
+
return await result
|
|
56
|
+
|
|
57
|
+
# TODO(krishan711): figure out correct typing here
|
|
58
|
+
return async_wrapper # type: ignore[return-value]
|
|
59
|
+
|
|
60
|
+
return decorator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def get_basic_authentication_from_authorization_signature[ApiRequest: BaseModel](request: KibaApiRequest[ApiRequest], authorizer: SignatureAuthorizer) -> BasicAuthentication:
|
|
64
|
+
authorization = request.headers.get('Authorization')
|
|
65
|
+
if not authorization:
|
|
66
|
+
raise ForbiddenException(message='AUTH_NOT_PROVIDED')
|
|
67
|
+
if not authorization.startswith('Signature '):
|
|
68
|
+
raise ForbiddenException(message='AUTH_INVALID')
|
|
69
|
+
signatureString = authorization.replace('Signature ', '', 1)
|
|
70
|
+
try:
|
|
71
|
+
signerId = await authorizer.retrieve_signature_signer(signatureString=signatureString)
|
|
72
|
+
except UnauthorizedException:
|
|
73
|
+
raise
|
|
74
|
+
except BaseException as exception: # noqa: BLE001
|
|
75
|
+
logging.exception(exception) # type: ignore[arg-type]
|
|
76
|
+
raise ForbiddenException(message='AUTH_INVALID')
|
|
77
|
+
return BasicAuthentication(username=signerId, password=signatureString)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def authorize_signature[ApiRequest: BaseModel]( # type: ignore[explicit-any]
|
|
81
|
+
authorizer: SignatureAuthorizer,
|
|
82
|
+
) -> typing.Callable[[typing.Callable[[Arg(KibaApiRequest[ApiRequest], 'request')], _AnyReturn]], typing.Callable[_P, typing.Any]]:
|
|
83
|
+
def decorator(func: typing.Callable[[Arg(KibaApiRequest[ApiRequest], 'request')], _AnyReturn]) -> typing.Callable[_P, typing.Any]: # type: ignore[explicit-any]
|
|
84
|
+
@functools.wraps(func)
|
|
85
|
+
async def async_wrapper(request: KibaApiRequest[ApiRequest]) -> typing.Any: # type: ignore[explicit-any, misc]
|
|
86
|
+
request.authBasic = await get_basic_authentication_from_authorization_signature(request=request, authorizer=authorizer)
|
|
87
|
+
result = func(request=request)
|
|
88
|
+
# NOTE(krishan711): this is here to support streaming responses which return an async generator
|
|
89
|
+
if hasattr(result, '__aiter__'):
|
|
90
|
+
return result
|
|
91
|
+
return await result
|
|
92
|
+
|
|
93
|
+
# TODO(krishan711): figure out correct typing here
|
|
94
|
+
return async_wrapper # type: ignore[return-value]
|
|
95
|
+
|
|
96
|
+
return decorator
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TokenAuthorizer:
|
|
100
|
+
async def validate_token(self, token: str) -> None:
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class StaticTokenAuthorizer(TokenAuthorizer):
|
|
105
|
+
def __init__(self, token: str) -> None:
|
|
106
|
+
self._token = token
|
|
107
|
+
|
|
108
|
+
async def validate_token(self, token: str) -> None:
|
|
109
|
+
if token != self._token:
|
|
110
|
+
raise ForbiddenException(message='AUTH_INVALID')
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def authorize_token[ApiRequest: BaseModel]( # type: ignore[explicit-any]
|
|
114
|
+
authorizer: TokenAuthorizer,
|
|
115
|
+
) -> typing.Callable[[typing.Callable[[Arg(KibaApiRequest[ApiRequest], 'request')], _AnyReturn]], typing.Callable[_P, typing.Any]]:
|
|
116
|
+
def decorator(func: typing.Callable[[Arg(KibaApiRequest[ApiRequest], 'request')], _AnyReturn]) -> typing.Callable[_P, typing.Any]: # type: ignore[explicit-any]
|
|
117
|
+
@functools.wraps(func)
|
|
118
|
+
async def async_wrapper(request: KibaApiRequest[ApiRequest]) -> typing.Any: # type: ignore[explicit-any, misc]
|
|
119
|
+
authorization = request.headers.get('Authorization')
|
|
120
|
+
if not authorization:
|
|
121
|
+
raise ForbiddenException(message='AUTH_NOT_PROVIDED')
|
|
122
|
+
if not authorization.startswith('Token '):
|
|
123
|
+
raise ForbiddenException(message='AUTH_INVALID')
|
|
124
|
+
await authorizer.validate_token(authorization[6:])
|
|
125
|
+
result = func(request=request)
|
|
126
|
+
# NOTE(krishan711): this is here to support streaming responses which return an async generator
|
|
127
|
+
if hasattr(result, '__aiter__'):
|
|
128
|
+
return result
|
|
129
|
+
return await result
|
|
130
|
+
|
|
131
|
+
# TODO(krishan711): figure out correct typing here
|
|
132
|
+
return async_wrapper # type: ignore[return-value]
|
|
133
|
+
|
|
134
|
+
return decorator
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from starlette.types import ASGIApp
|
|
2
|
+
from starlette.types import Receive
|
|
3
|
+
from starlette.types import Scope
|
|
4
|
+
from starlette.types import Send
|
|
5
|
+
|
|
6
|
+
from core.store.database import Database
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabaseConnectionMiddleware:
|
|
10
|
+
def __init__(self, app: ASGIApp, database: Database) -> None:
|
|
11
|
+
self.app = app
|
|
12
|
+
self.database = database
|
|
13
|
+
|
|
14
|
+
# NOTE(krishan711): see note in database.py about why this can cause problems with concurrent operations
|
|
15
|
+
# NOTE(krishan711): raw ASGI (not BaseHTTPMiddleware) so the DB connection stays open across streaming body
|
|
16
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
17
|
+
if scope['type'] != 'http':
|
|
18
|
+
await self.app(scope, receive, send)
|
|
19
|
+
return
|
|
20
|
+
async with self.database.create_context_connection():
|
|
21
|
+
await self.app(scope, receive, send)
|
|
@@ -76,6 +76,7 @@ kiba_core.egg-info/dependency_links.txt
|
|
|
76
76
|
kiba_core.egg-info/requires.txt
|
|
77
77
|
kiba_core.egg-info/top_level.txt
|
|
78
78
|
tests/__init__.py
|
|
79
|
+
tests/api/test_authorizer.py
|
|
79
80
|
tests/api/test_json_route.py
|
|
80
81
|
tests/api/test_streaming_json_route.py
|
|
81
82
|
tests/caching/test_dict_cache.py
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pytest
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from starlette.applications import Starlette
|
|
6
|
+
from starlette.routing import Route
|
|
7
|
+
from starlette.testclient import TestClient
|
|
8
|
+
|
|
9
|
+
from core.api.api_request import KibaApiRequest
|
|
10
|
+
from core.api.authorizer import Authorizer, SignatureAuthorizer, StaticTokenAuthorizer, authorize_bearer_jwt, authorize_signature, authorize_token
|
|
11
|
+
from core.api.json_route import json_route
|
|
12
|
+
from core.api.middleware.exception_handling_middleware import ExceptionHandlingMiddleware
|
|
13
|
+
from core.api.streaming_json_route import streaming_json_route
|
|
14
|
+
from core.exceptions import ForbiddenException
|
|
15
|
+
from core.http.jwt import Jwt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
VALID_JWT_TOKEN = 'valid-token'
|
|
19
|
+
VALID_SIGNATURE = 'valid-sig'
|
|
20
|
+
VALID_STATIC_TOKEN = 'secret-token'
|
|
21
|
+
VALID_USER_ID = 'user-123'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MockJwtAuthorizer(Authorizer):
|
|
25
|
+
async def validate_jwt(self, jwtString: str) -> Jwt:
|
|
26
|
+
if jwtString != VALID_JWT_TOKEN:
|
|
27
|
+
raise ForbiddenException('Invalid token')
|
|
28
|
+
jwt = Jwt(payloadDict={'sub': VALID_USER_ID})
|
|
29
|
+
jwt.userId = VALID_USER_ID # type: ignore[attr-defined]
|
|
30
|
+
return jwt
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MockSignatureAuthorizer(SignatureAuthorizer):
|
|
34
|
+
async def retrieve_signature_signer(self, signatureString: str) -> str:
|
|
35
|
+
if signatureString != VALID_SIGNATURE:
|
|
36
|
+
raise ForbiddenException('Invalid signature')
|
|
37
|
+
return VALID_USER_ID
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SimpleRequest(BaseModel):
|
|
41
|
+
value: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SimpleResponse(BaseModel):
|
|
45
|
+
result: str
|
|
46
|
+
user_id: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
jwt_authorizer = MockJwtAuthorizer()
|
|
50
|
+
sig_authorizer = MockSignatureAuthorizer()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --- authorize_bearer_jwt fixtures ---
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def jwt_json_client():
|
|
57
|
+
@json_route(requestType=SimpleRequest, responseType=SimpleResponse)
|
|
58
|
+
@authorize_bearer_jwt(authorizer=jwt_authorizer)
|
|
59
|
+
async def protected_endpoint(request: KibaApiRequest[SimpleRequest]) -> SimpleResponse:
|
|
60
|
+
jwt = request.authJwt
|
|
61
|
+
return SimpleResponse(result=request.data.value, user_id=getattr(jwt, 'userId', None))
|
|
62
|
+
|
|
63
|
+
app = Starlette(routes=[Route('/protected', protected_endpoint, methods=['POST'])])
|
|
64
|
+
app.add_middleware(ExceptionHandlingMiddleware)
|
|
65
|
+
return TestClient(app, raise_server_exceptions=False)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.fixture
|
|
69
|
+
def jwt_streaming_client():
|
|
70
|
+
@streaming_json_route(requestType=SimpleRequest, responseType=SimpleResponse)
|
|
71
|
+
@authorize_bearer_jwt(authorizer=jwt_authorizer)
|
|
72
|
+
async def protected_streaming_endpoint(request: KibaApiRequest[SimpleRequest]) -> AsyncIterator[SimpleResponse]:
|
|
73
|
+
jwt = request.authJwt
|
|
74
|
+
yield SimpleResponse(result=request.data.value, user_id=getattr(jwt, 'userId', None))
|
|
75
|
+
|
|
76
|
+
app = Starlette(routes=[Route('/protected-stream', protected_streaming_endpoint, methods=['POST'])])
|
|
77
|
+
app.add_middleware(ExceptionHandlingMiddleware)
|
|
78
|
+
return TestClient(app, raise_server_exceptions=False)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --- authorize_signature fixtures ---
|
|
82
|
+
|
|
83
|
+
@pytest.fixture
|
|
84
|
+
def sig_json_client():
|
|
85
|
+
@json_route(requestType=SimpleRequest, responseType=SimpleResponse)
|
|
86
|
+
@authorize_signature(authorizer=sig_authorizer)
|
|
87
|
+
async def protected_endpoint(request: KibaApiRequest[SimpleRequest]) -> SimpleResponse:
|
|
88
|
+
return SimpleResponse(result=request.data.value, user_id=request.authBasic.username if request.authBasic else None)
|
|
89
|
+
|
|
90
|
+
app = Starlette(routes=[Route('/protected', protected_endpoint, methods=['POST'])])
|
|
91
|
+
app.add_middleware(ExceptionHandlingMiddleware)
|
|
92
|
+
return TestClient(app, raise_server_exceptions=False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.fixture
|
|
96
|
+
def sig_streaming_client():
|
|
97
|
+
@streaming_json_route(requestType=SimpleRequest, responseType=SimpleResponse)
|
|
98
|
+
@authorize_signature(authorizer=sig_authorizer)
|
|
99
|
+
async def protected_streaming_endpoint(request: KibaApiRequest[SimpleRequest]) -> AsyncIterator[SimpleResponse]:
|
|
100
|
+
yield SimpleResponse(result=request.data.value, user_id=request.authBasic.username if request.authBasic else None)
|
|
101
|
+
|
|
102
|
+
app = Starlette(routes=[Route('/protected-stream', protected_streaming_endpoint, methods=['POST'])])
|
|
103
|
+
app.add_middleware(ExceptionHandlingMiddleware)
|
|
104
|
+
return TestClient(app, raise_server_exceptions=False)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- authorize_token fixtures ---
|
|
108
|
+
|
|
109
|
+
token_authorizer = StaticTokenAuthorizer(token=VALID_STATIC_TOKEN)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.fixture
|
|
113
|
+
def token_json_client():
|
|
114
|
+
@json_route(requestType=SimpleRequest, responseType=SimpleResponse)
|
|
115
|
+
@authorize_token(authorizer=token_authorizer)
|
|
116
|
+
async def protected_endpoint(request: KibaApiRequest[SimpleRequest]) -> SimpleResponse:
|
|
117
|
+
return SimpleResponse(result=request.data.value)
|
|
118
|
+
|
|
119
|
+
app = Starlette(routes=[Route('/protected', protected_endpoint, methods=['POST'])])
|
|
120
|
+
app.add_middleware(ExceptionHandlingMiddleware)
|
|
121
|
+
return TestClient(app, raise_server_exceptions=False)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.fixture
|
|
125
|
+
def token_streaming_client():
|
|
126
|
+
@streaming_json_route(requestType=SimpleRequest, responseType=SimpleResponse)
|
|
127
|
+
@authorize_token(authorizer=token_authorizer)
|
|
128
|
+
async def protected_streaming_endpoint(request: KibaApiRequest[SimpleRequest]) -> AsyncIterator[SimpleResponse]:
|
|
129
|
+
yield SimpleResponse(result=request.data.value)
|
|
130
|
+
|
|
131
|
+
app = Starlette(routes=[Route('/protected-stream', protected_streaming_endpoint, methods=['POST'])])
|
|
132
|
+
app.add_middleware(ExceptionHandlingMiddleware)
|
|
133
|
+
return TestClient(app, raise_server_exceptions=False)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --- authorize_bearer_jwt + json_route ---
|
|
137
|
+
|
|
138
|
+
def test_jwt_json_no_auth_header_returns_403(jwt_json_client):
|
|
139
|
+
response = jwt_json_client.post('/protected', json={'value': 'hello'})
|
|
140
|
+
assert response.status_code == 403
|
|
141
|
+
|
|
142
|
+
def test_jwt_json_wrong_scheme_returns_403(jwt_json_client):
|
|
143
|
+
response = jwt_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': 'Basic creds'})
|
|
144
|
+
assert response.status_code == 403
|
|
145
|
+
|
|
146
|
+
def test_jwt_json_invalid_token_returns_403(jwt_json_client):
|
|
147
|
+
response = jwt_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': 'Bearer bad-token'})
|
|
148
|
+
assert response.status_code == 403
|
|
149
|
+
|
|
150
|
+
def test_jwt_json_valid_token_returns_200(jwt_json_client):
|
|
151
|
+
response = jwt_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': f'Bearer {VALID_JWT_TOKEN}'})
|
|
152
|
+
assert response.status_code == 200
|
|
153
|
+
assert response.json()['result'] == 'hello'
|
|
154
|
+
|
|
155
|
+
def test_jwt_json_sets_auth_jwt_on_request(jwt_json_client):
|
|
156
|
+
response = jwt_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': f'Bearer {VALID_JWT_TOKEN}'})
|
|
157
|
+
assert response.status_code == 200
|
|
158
|
+
assert response.json()['user_id'] == VALID_USER_ID
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- authorize_bearer_jwt + streaming_json_route ---
|
|
162
|
+
|
|
163
|
+
def test_jwt_streaming_no_auth_header_returns_403(jwt_streaming_client):
|
|
164
|
+
response = jwt_streaming_client.post('/protected-stream', json={'value': 'hello'})
|
|
165
|
+
assert response.status_code == 403
|
|
166
|
+
|
|
167
|
+
def test_jwt_streaming_invalid_token_returns_403(jwt_streaming_client):
|
|
168
|
+
response = jwt_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': 'Bearer bad-token'})
|
|
169
|
+
assert response.status_code == 403
|
|
170
|
+
|
|
171
|
+
def test_jwt_streaming_valid_token_returns_200(jwt_streaming_client):
|
|
172
|
+
response = jwt_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': f'Bearer {VALID_JWT_TOKEN}'})
|
|
173
|
+
assert response.status_code == 200
|
|
174
|
+
|
|
175
|
+
def test_jwt_streaming_valid_token_streams_data(jwt_streaming_client):
|
|
176
|
+
response = jwt_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': f'Bearer {VALID_JWT_TOKEN}'})
|
|
177
|
+
data = json.loads(response.content.decode().strip())
|
|
178
|
+
assert data['result'] == 'hello'
|
|
179
|
+
assert data['user_id'] == VALID_USER_ID
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --- authorize_signature + json_route ---
|
|
183
|
+
|
|
184
|
+
def test_sig_json_no_auth_header_returns_403(sig_json_client):
|
|
185
|
+
response = sig_json_client.post('/protected', json={'value': 'hello'})
|
|
186
|
+
assert response.status_code == 403
|
|
187
|
+
|
|
188
|
+
def test_sig_json_wrong_scheme_returns_403(sig_json_client):
|
|
189
|
+
response = sig_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': 'Bearer something'})
|
|
190
|
+
assert response.status_code == 403
|
|
191
|
+
|
|
192
|
+
def test_sig_json_invalid_signature_returns_403(sig_json_client):
|
|
193
|
+
response = sig_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': 'Signature bad-sig'})
|
|
194
|
+
assert response.status_code == 403
|
|
195
|
+
|
|
196
|
+
def test_sig_json_valid_signature_returns_200(sig_json_client):
|
|
197
|
+
response = sig_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': f'Signature {VALID_SIGNATURE}'})
|
|
198
|
+
assert response.status_code == 200
|
|
199
|
+
assert response.json()['result'] == 'hello'
|
|
200
|
+
|
|
201
|
+
def test_sig_json_sets_auth_basic_on_request(sig_json_client):
|
|
202
|
+
response = sig_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': f'Signature {VALID_SIGNATURE}'})
|
|
203
|
+
assert response.status_code == 200
|
|
204
|
+
assert response.json()['user_id'] == VALID_USER_ID
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# --- authorize_signature + streaming_json_route ---
|
|
208
|
+
|
|
209
|
+
def test_sig_streaming_no_auth_header_returns_403(sig_streaming_client):
|
|
210
|
+
response = sig_streaming_client.post('/protected-stream', json={'value': 'hello'})
|
|
211
|
+
assert response.status_code == 403
|
|
212
|
+
|
|
213
|
+
def test_sig_streaming_invalid_signature_returns_403(sig_streaming_client):
|
|
214
|
+
response = sig_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': 'Signature bad-sig'})
|
|
215
|
+
assert response.status_code == 403
|
|
216
|
+
|
|
217
|
+
def test_sig_streaming_valid_signature_returns_200(sig_streaming_client):
|
|
218
|
+
response = sig_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': f'Signature {VALID_SIGNATURE}'})
|
|
219
|
+
assert response.status_code == 200
|
|
220
|
+
|
|
221
|
+
def test_sig_streaming_valid_signature_streams_data(sig_streaming_client):
|
|
222
|
+
response = sig_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': f'Signature {VALID_SIGNATURE}'})
|
|
223
|
+
data = json.loads(response.content.decode().strip())
|
|
224
|
+
assert data['result'] == 'hello'
|
|
225
|
+
assert data['user_id'] == VALID_USER_ID
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# --- authorize_token + json_route ---
|
|
229
|
+
|
|
230
|
+
def test_token_json_no_auth_header_returns_403(token_json_client):
|
|
231
|
+
response = token_json_client.post('/protected', json={'value': 'hello'})
|
|
232
|
+
assert response.status_code == 403
|
|
233
|
+
|
|
234
|
+
def test_token_json_wrong_scheme_returns_403(token_json_client):
|
|
235
|
+
response = token_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': 'Bearer something'})
|
|
236
|
+
assert response.status_code == 403
|
|
237
|
+
|
|
238
|
+
def test_token_json_invalid_token_returns_403(token_json_client):
|
|
239
|
+
response = token_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': 'Token wrong-token'})
|
|
240
|
+
assert response.status_code == 403
|
|
241
|
+
|
|
242
|
+
def test_token_json_valid_token_returns_200(token_json_client):
|
|
243
|
+
response = token_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': f'Token {VALID_STATIC_TOKEN}'})
|
|
244
|
+
assert response.status_code == 200
|
|
245
|
+
assert response.json()['result'] == 'hello'
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# --- authorize_token + streaming_json_route ---
|
|
249
|
+
|
|
250
|
+
def test_token_streaming_no_auth_header_returns_403(token_streaming_client):
|
|
251
|
+
response = token_streaming_client.post('/protected-stream', json={'value': 'hello'})
|
|
252
|
+
assert response.status_code == 403
|
|
253
|
+
|
|
254
|
+
def test_token_streaming_invalid_token_returns_403(token_streaming_client):
|
|
255
|
+
response = token_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': 'Token wrong-token'})
|
|
256
|
+
assert response.status_code == 403
|
|
257
|
+
|
|
258
|
+
def test_token_streaming_valid_token_returns_200(token_streaming_client):
|
|
259
|
+
response = token_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': f'Token {VALID_STATIC_TOKEN}'})
|
|
260
|
+
assert response.status_code == 200
|
|
261
|
+
|
|
262
|
+
def test_token_streaming_valid_token_streams_data(token_streaming_client):
|
|
263
|
+
response = token_streaming_client.post('/protected-stream', json={'value': 'hello'}, headers={'Authorization': f'Token {VALID_STATIC_TOKEN}'})
|
|
264
|
+
data = json.loads(response.content.decode().strip())
|
|
265
|
+
assert data['result'] == 'hello'
|