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.
Files changed (99) hide show
  1. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/Dockerfile +0 -1
  2. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/PKG-INFO +1 -1
  3. kiba_core-0.5.3.dev49/core/api/authorizer.py +134 -0
  4. kiba_core-0.5.3.dev49/core/api/middleware/database_connection_middleware.py +21 -0
  5. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/PKG-INFO +1 -1
  6. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/SOURCES.txt +1 -0
  7. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/pyproject.toml +1 -1
  8. kiba_core-0.5.3.dev49/tests/api/test_authorizer.py +265 -0
  9. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/uv.lock +704 -686
  10. kiba_core-0.5.3.dev47/core/api/authorizer.py +0 -50
  11. kiba_core-0.5.3.dev47/core/api/middleware/database_connection_middleware.py +0 -24
  12. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/pull_request_template.md +0 -0
  13. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/workflows/deploy.yml +0 -0
  14. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/workflows/pull-request.yml +0 -0
  15. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.github/workflows/release.yml +0 -0
  16. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/.gitignore +0 -0
  17. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/CHANGELOG.md +0 -0
  18. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/README.md +0 -0
  19. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/__init__.py +0 -0
  20. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/__init__.py +0 -0
  21. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/api_request.py +0 -0
  22. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/api_response.py +0 -0
  23. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/default_routes.py +0 -0
  24. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/health.py +0 -0
  25. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/json_route.py +0 -0
  26. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/__init__.py +0 -0
  27. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/exception_handling_middleware.py +0 -0
  28. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/logging_middleware.py +0 -0
  29. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/middleware/server_headers_middleware.py +0 -0
  30. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/api/streaming_json_route.py +0 -0
  31. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/aws_requester.py +0 -0
  32. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/__init__.py +0 -0
  33. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/cache.py +0 -0
  34. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/dict_cache.py +0 -0
  35. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/caching/file_cache.py +0 -0
  36. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/exceptions.py +0 -0
  37. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/__init__.py +0 -0
  38. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/basic_authentication.py +0 -0
  39. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/jwt.py +0 -0
  40. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/http/rest_method.py +0 -0
  41. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/logging.py +0 -0
  42. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/__init__.py +0 -0
  43. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/discord_client.py +0 -0
  44. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/notification_client.py +0 -0
  45. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/notifications/slack_client.py +0 -0
  46. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/py.typed +0 -0
  47. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/__init__.py +0 -0
  48. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/aqs.py +0 -0
  49. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/message_queue.py +0 -0
  50. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/message_queue_processor.py +0 -0
  51. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/model.py +0 -0
  52. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/queues/sqs.py +0 -0
  53. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/requester/__init__.py +0 -0
  54. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/requester/requester.py +0 -0
  55. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/s3_manager.py +0 -0
  56. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/service_client.py +0 -0
  57. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/__init__.py +0 -0
  58. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/database.py +0 -0
  59. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/retriever.py +0 -0
  60. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/store/saver.py +0 -0
  61. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/__init__.py +0 -0
  62. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/async_util.py +0 -0
  63. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/chain_util.py +0 -0
  64. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/date_util.py +0 -0
  65. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/dict_util.py +0 -0
  66. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/file_util.py +0 -0
  67. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/hashing_util.py +0 -0
  68. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/http_util.py +0 -0
  69. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/json_util.py +0 -0
  70. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/list_util.py +0 -0
  71. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/string_util.py +0 -0
  72. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/typing_util.py +0 -0
  73. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/url_util.py +0 -0
  74. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/util/value_holder.py +0 -0
  75. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/web3/__init__.py +0 -0
  76. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/web3/eth_client.py +0 -0
  77. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/core/web3/multicall3.py +0 -0
  78. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/dependency_links.txt +0 -0
  79. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/requires.txt +0 -0
  80. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/kiba_core.egg-info/top_level.txt +0 -0
  81. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/makefile +0 -0
  82. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/setup.cfg +0 -0
  83. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/__init__.py +0 -0
  84. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/api/test_json_route.py +0 -0
  85. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/api/test_streaming_json_route.py +0 -0
  86. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/caching/test_dict_cache.py +0 -0
  87. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/caching/test_file_cache.py +0 -0
  88. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/__init__.py +0 -0
  89. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_async_util.py +0 -0
  90. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_chain_util.py +0 -0
  91. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_date_util.py +0 -0
  92. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_dict_util.py +0 -0
  93. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_file_util.py +0 -0
  94. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_json_util.py +0 -0
  95. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_list_util.py +0 -0
  96. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_string_util.py +0 -0
  97. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/util/test_url_util.py +0 -0
  98. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/web3/__init__.py +0 -0
  99. {kiba_core-0.5.3.dev47 → kiba_core-0.5.3.dev49}/tests/web3/test_rest_eth_client.py +0 -0
@@ -10,4 +10,3 @@ COPY uv.lock .
10
10
  RUN make install
11
11
 
12
12
  COPY . .
13
- RUN make type-check
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiba-core
3
- Version: 0.5.3.dev47
3
+ Version: 0.5.3.dev49
4
4
  Summary: Kiba Labs' python utilities
5
5
  Project-URL: repository, https://github.com/kibalabs/core-py
6
6
  Requires-Python: <4,>=3.12
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiba-core
3
- Version: 0.5.3.dev47
3
+ Version: 0.5.3.dev49
4
4
  Summary: Kiba Labs' python utilities
5
5
  Project-URL: repository, https://github.com/kibalabs/core-py
6
6
  Requires-Python: <4,>=3.12
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiba-core"
3
- version = "0.5.3.dev47"
3
+ version = "0.5.3.dev49"
4
4
  description = "Kiba Labs' python utilities"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12, <4"
@@ -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'