kiba-core 0.5.3.dev66__tar.gz → 0.5.3.dev67__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 (129) hide show
  1. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/CHANGELOG.md +2 -0
  2. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/PKG-INFO +1 -1
  3. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/authorizer.py +9 -7
  4. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/route.py +5 -3
  5. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/kiba_core.egg-info/PKG-INFO +1 -1
  6. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/pyproject.toml +1 -1
  7. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_authorizer.py +32 -1
  8. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_route.py +24 -6
  9. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/.github/pull_request_template.md +0 -0
  10. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/.github/workflows/deploy.yml +0 -0
  11. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/.github/workflows/pull-request.yml +0 -0
  12. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/.github/workflows/release.yml +0 -0
  13. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/.gitignore +0 -0
  14. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/Dockerfile +0 -0
  15. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/README.md +0 -0
  16. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/__init__.py +0 -0
  17. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/__init__.py +0 -0
  18. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/api_request.py +0 -0
  19. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/api_response.py +0 -0
  20. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/default_routes.py +0 -0
  21. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/health.py +0 -0
  22. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/json_route.py +0 -0
  23. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/middleware/__init__.py +0 -0
  24. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/middleware/database_connection_middleware.py +0 -0
  25. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/middleware/exception_handling_middleware.py +0 -0
  26. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/middleware/logging_middleware.py +0 -0
  27. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/middleware/origin_ip_middleware.py +0 -0
  28. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/middleware/server_headers_middleware.py +0 -0
  29. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/openapi.py +0 -0
  30. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/rate_limit.py +0 -0
  31. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/request_context.py +0 -0
  32. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/route_auth.py +0 -0
  33. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/route_metadata.py +0 -0
  34. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/api/streaming_json_route.py +0 -0
  35. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/aws_requester.py +0 -0
  36. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/caching/__init__.py +0 -0
  37. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/caching/cache.py +0 -0
  38. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/caching/dict_cache.py +0 -0
  39. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/caching/file_cache.py +0 -0
  40. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/exceptions.py +0 -0
  41. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/http/__init__.py +0 -0
  42. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/http/basic_authentication.py +0 -0
  43. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/http/jwt.py +0 -0
  44. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/http/rest_method.py +0 -0
  45. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/locks/__init__.py +0 -0
  46. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/locks/lock.py +0 -0
  47. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/locks/model.py +0 -0
  48. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/locks/sql.py +0 -0
  49. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/logging.py +0 -0
  50. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/notifications/__init__.py +0 -0
  51. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/notifications/discord_client.py +0 -0
  52. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/notifications/notification_client.py +0 -0
  53. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/notifications/slack_client.py +0 -0
  54. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/py.typed +0 -0
  55. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/__init__.py +0 -0
  56. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/aqs.py +0 -0
  57. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/cosmos.py +0 -0
  58. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/message_queue.py +0 -0
  59. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/message_queue_processor.py +0 -0
  60. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/model.py +0 -0
  61. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/sql.py +0 -0
  62. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/queues/sqs.py +0 -0
  63. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/requester/__init__.py +0 -0
  64. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/requester/requester.py +0 -0
  65. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/s3_manager.py +0 -0
  66. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/service_client.py +0 -0
  67. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/store/__init__.py +0 -0
  68. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/store/database.py +0 -0
  69. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/store/retriever.py +0 -0
  70. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/store/saver.py +0 -0
  71. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/__init__.py +0 -0
  72. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/async_util.py +0 -0
  73. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/chain_util.py +0 -0
  74. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/date_util.py +0 -0
  75. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/dict_util.py +0 -0
  76. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/file_util.py +0 -0
  77. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/hashing_util.py +0 -0
  78. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/http_util.py +0 -0
  79. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/json_util.py +0 -0
  80. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/list_util.py +0 -0
  81. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/process_util.py +0 -0
  82. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/string_util.py +0 -0
  83. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/typing_util.py +0 -0
  84. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/url_util.py +0 -0
  85. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/util/value_holder.py +0 -0
  86. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/web3/__init__.py +0 -0
  87. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/web3/eth_client.py +0 -0
  88. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/core/web3/multicall3.py +0 -0
  89. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/kiba_core.egg-info/SOURCES.txt +0 -0
  90. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/kiba_core.egg-info/dependency_links.txt +0 -0
  91. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/kiba_core.egg-info/requires.txt +0 -0
  92. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/kiba_core.egg-info/top_level.txt +0 -0
  93. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/makefile +0 -0
  94. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/setup.cfg +0 -0
  95. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/__init__.py +0 -0
  96. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_api_response.py +0 -0
  97. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_exception_handling_middleware.py +0 -0
  98. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_json_route.py +0 -0
  99. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_openapi.py +0 -0
  100. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_origin_ip_middleware.py +0 -0
  101. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_rate_limit.py +0 -0
  102. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_request_context.py +0 -0
  103. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/api/test_streaming_json_route.py +0 -0
  104. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/caching/test_dict_cache.py +0 -0
  105. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/caching/test_file_cache.py +0 -0
  106. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/locks/test_sql_lock.py +0 -0
  107. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/queues/conftest.py +0 -0
  108. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/queues/test_aqs_queue.py +0 -0
  109. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/queues/test_cosmos_queue.py +0 -0
  110. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/queues/test_sql_queue.py +0 -0
  111. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/queues/test_sqs_queue.py +0 -0
  112. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/requester/__init__.py +0 -0
  113. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/requester/test_requester.py +0 -0
  114. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/test_database.py +0 -0
  115. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/test_exceptions.py +0 -0
  116. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/__init__.py +0 -0
  117. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_async_util.py +0 -0
  118. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_chain_util.py +0 -0
  119. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_date_util.py +0 -0
  120. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_dict_util.py +0 -0
  121. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_file_util.py +0 -0
  122. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_json_util.py +0 -0
  123. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_list_util.py +0 -0
  124. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_process_util.py +0 -0
  125. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_string_util.py +0 -0
  126. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/util/test_url_util.py +0 -0
  127. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/web3/__init__.py +0 -0
  128. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/tests/web3/test_rest_eth_client.py +0 -0
  129. {kiba_core-0.5.3.dev66 → kiba_core-0.5.3.dev67}/uv.lock +0 -0
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
8
8
  ## [Unreleased]
9
9
 
10
10
  ### Added
11
+ - [MINOR] Added `authorize_bearer_jwt_request` for validating bearer JWTs in `RouteAuthResolver` implementations; it and `authorize_bearer_jwt` now only strip the leading `Bearer ` prefix, let cancellation propagate, and log why a JWT was rejected
12
+ - [MINOR] Made the `route` `authResolver` optional for routes without `auth`; setting `auth` without an `authResolver` raises `ValueError` at registration
11
13
  - [MAJOR] Replaced `MessageQueue.delete_message` with `complete_message`, and added abstract `retry_message` (reschedule with a delay) and `fail_message` (apply the queue's failure policy); `MessageQueueProcessor` now reschedules `MessageNeedsReprocessingException` messages with `retry_message`
12
14
  - [MINOR] Updated `MessageQueueProcessor` to retry messages that raise `LockedException` up to 3 times (30s linear backoff) before failing them
13
15
  - [MINOR] Updated `MessageQueueProcessor` to log instead of raise when a failure notification cannot be sent
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiba-core
3
- Version: 0.5.3.dev66
3
+ Version: 0.5.3.dev67
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
@@ -10,6 +10,7 @@ from core.api.api_request import KibaApiRequest
10
10
  from core.api.route_metadata import OpenApiSecurityScheme
11
11
  from core.api.route_metadata import update_route_metadata
12
12
  from core.exceptions import ForbiddenException
13
+ from core.exceptions import KibaException
13
14
  from core.exceptions import UnauthorizedException
14
15
  from core.http.basic_authentication import BasicAuthentication
15
16
  from core.http.jwt import Jwt
@@ -27,18 +28,19 @@ class SignatureAuthorizer:
27
28
  raise NotImplementedError
28
29
 
29
30
 
30
- async def _authorize_bearer_jwt[ApiRequest: BaseModel](request: KibaApiRequest[ApiRequest], authorizer: Authorizer) -> Jwt:
31
+ async def authorize_bearer_jwt_request[ApiRequest: BaseModel](request: KibaApiRequest[ApiRequest], authorizer: Authorizer) -> Jwt:
31
32
  authorization = request.headers.get('Authorization')
32
33
  if not authorization:
33
34
  raise ForbiddenException(message='AUTH_NOT_PROVIDED')
34
35
  if not authorization.startswith('Bearer '):
35
36
  raise ForbiddenException(message='AUTH_INVALID')
36
- jwtString = authorization.replace('Bearer ', '')
37
37
  try:
38
- jwt = await authorizer.validate_jwt(jwtString=jwtString)
39
- except BaseException: # noqa: BLE001
40
- raise ForbiddenException(message='AUTH_INVALID')
41
- return jwt
38
+ return await authorizer.validate_jwt(jwtString=authorization.removeprefix('Bearer '))
39
+ except Exception as exception:
40
+ # NOTE(krishan711): only log kiba messages as other exception messages (e.g. json decode errors) can contain parts of the token
41
+ reason = exception.message if isinstance(exception, KibaException) else type(exception).__name__
42
+ logging.info(f'Bearer JWT rejected: {reason}')
43
+ raise ForbiddenException(message='AUTH_INVALID') from exception
42
44
 
43
45
 
44
46
  def authorize_bearer_jwt[ApiRequest: BaseModel]( # type: ignore[explicit-any]
@@ -52,7 +54,7 @@ def authorize_bearer_jwt[ApiRequest: BaseModel]( # type: ignore[explicit-any]
52
54
 
53
55
  @functools.wraps(func)
54
56
  async def async_wrapper(request: KibaApiRequest[ApiRequest]) -> typing.Any: # type: ignore[explicit-any, misc]
55
- request.authJwt = await _authorize_bearer_jwt(request=request, authorizer=authorizer)
57
+ request.authJwt = await authorize_bearer_jwt_request(request=request, authorizer=authorizer)
56
58
  result = func(request)
57
59
  # NOTE(krishan711): this is here to support streaming responses which return an async generator
58
60
  if hasattr(result, '__aiter__'):
@@ -42,7 +42,7 @@ def route[ApiRequest: BaseModel, ApiResponse: BaseModel]( # type: ignore[explic
42
42
  requestType: typing.Type[ApiRequest],
43
43
  responseType: typing.Type[ApiResponse],
44
44
  *,
45
- authResolver: RouteAuthResolver,
45
+ authResolver: RouteAuthResolver | None = None,
46
46
  isStreaming: bool = False,
47
47
  operationId: str | None = None,
48
48
  summary: str | None = None,
@@ -52,13 +52,15 @@ def route[ApiRequest: BaseModel, ApiResponse: BaseModel]( # type: ignore[explic
52
52
  rateLimit: RateLimitConfig | None = None,
53
53
  extensions: Mapping[str, object] | None = None,
54
54
  ) -> typing.Callable[[typing.Callable[[KibaApiRequest[ApiRequest]], _AnyReturn]], typing.Callable[[Request], typing.Awaitable[KibaJSONResponse | StreamingResponse]]]:
55
- securitySchemeNames = authResolver.get_route_security_schemes(auth=auth) if auth is not None else []
56
-
57
55
  def decorator(func: typing.Callable[[KibaApiRequest[ApiRequest]], _AnyReturn]) -> typing.Callable[[Request], typing.Awaitable[KibaJSONResponse | StreamingResponse]]: # type: ignore[explicit-any]
58
56
  handler: typing.Callable[[KibaApiRequest[ApiRequest]], _AnyReturn] = func
59
57
  if rateLimit is not None:
60
58
  handler = rate_limit(rateLimit)(handler)
59
+ securitySchemeNames: list[str] = []
61
60
  if auth is not None:
61
+ if authResolver is None:
62
+ raise ValueError(f'Auth policy {auth} requires an authResolver')
63
+ securitySchemeNames = authResolver.get_route_security_schemes(auth=auth)
62
64
  handler = _authorize_route(authResolver, auth)(handler)
63
65
  endpointDecorator = streaming_json_route if isStreaming else json_route
64
66
  endpoint = endpointDecorator(requestType=requestType, responseType=responseType)(handler) # type: ignore[arg-type, ty:invalid-argument-type]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiba-core
3
- Version: 0.5.3.dev66
3
+ Version: 0.5.3.dev67
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiba-core"
3
- version = "0.5.3.dev66"
3
+ version = "0.5.3.dev67"
4
4
  description = "Kiba Labs' python utilities"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12, <4"
@@ -1,13 +1,15 @@
1
+ import asyncio
1
2
  import json
2
3
  import pytest
3
4
  from collections.abc import AsyncIterator
5
+ from types import SimpleNamespace
4
6
  from pydantic import BaseModel
5
7
  from starlette.applications import Starlette
6
8
  from starlette.routing import Route
7
9
  from starlette.testclient import TestClient
8
10
 
9
11
  from core.api.api_request import KibaApiRequest
10
- from core.api.authorizer import Authorizer, SignatureAuthorizer, StaticTokenAuthorizer, authorize_bearer_jwt, authorize_signature, authorize_static_header_request, authorize_token
12
+ from core.api.authorizer import Authorizer, SignatureAuthorizer, StaticTokenAuthorizer, authorize_bearer_jwt, authorize_bearer_jwt_request, authorize_signature, authorize_static_header_request, authorize_token
11
13
  from core.api.json_route import json_route
12
14
  from core.api.middleware.exception_handling_middleware import ExceptionHandlingMiddleware
13
15
  from core.api.streaming_json_route import streaming_json_route
@@ -159,6 +161,10 @@ def test_jwt_json_invalid_token_returns_403(jwt_json_client):
159
161
  response = jwt_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': 'Bearer bad-token'})
160
162
  assert response.status_code == 403
161
163
 
164
+ def test_jwt_json_repeated_bearer_prefix_returns_403(jwt_json_client):
165
+ response = jwt_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': f'Bearer Bearer {VALID_JWT_TOKEN}'})
166
+ assert response.status_code == 403
167
+
162
168
 
163
169
  def test_jwt_json_sets_auth_jwt_on_request(jwt_json_client):
164
170
  response = jwt_json_client.post('/protected', json={'value': 'hello'}, headers={'Authorization': f'Bearer {VALID_JWT_TOKEN}'})
@@ -186,6 +192,31 @@ def test_jwt_streaming_valid_token_streams_data(jwt_streaming_client):
186
192
  assert data['user_id'] == VALID_USER_ID
187
193
 
188
194
 
195
+ # --- authorize_bearer_jwt_request ---
196
+
197
+ class RaisingJwtAuthorizer(Authorizer):
198
+ def __init__(self, exception: BaseException) -> None:
199
+ self.exception = exception
200
+
201
+ async def validate_jwt(self, jwtString: str) -> Jwt:
202
+ raise self.exception
203
+
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_bearer_jwt_request_maps_unexpected_validation_errors_to_auth_invalid():
207
+ request = SimpleNamespace(headers={'Authorization': 'Bearer token'})
208
+ with pytest.raises(ForbiddenException) as exceptionInfo:
209
+ await authorize_bearer_jwt_request(request=request, authorizer=RaisingJwtAuthorizer(exception=ValueError('bad token')))
210
+ assert exceptionInfo.value.message == 'AUTH_INVALID'
211
+
212
+
213
+ @pytest.mark.asyncio
214
+ async def test_bearer_jwt_request_propagates_cancellation():
215
+ request = SimpleNamespace(headers={'Authorization': 'Bearer token'})
216
+ with pytest.raises(asyncio.CancelledError):
217
+ await authorize_bearer_jwt_request(request=request, authorizer=RaisingJwtAuthorizer(exception=asyncio.CancelledError()))
218
+
219
+
189
220
  # --- authorize_signature + json_route ---
190
221
 
191
222
  def test_sig_json_no_auth_header_returns_403(sig_json_client):
@@ -176,9 +176,27 @@ def test_route_static_token_policy_accepts_valid_credentials() -> None:
176
176
 
177
177
  def test_route_rejects_unknown_auth_policy_during_registration() -> None:
178
178
  with pytest.raises(ValueError, match='Unknown auth policy: unknown'):
179
- api_route(
180
- requestType=ExampleRequest,
181
- responseType=ExampleResponse,
182
- authResolver=ExampleRouteAuthResolver(),
183
- auth='unknown',
184
- )
179
+
180
+ @api_route(requestType=ExampleRequest, responseType=ExampleResponse, authResolver=ExampleRouteAuthResolver(), auth='unknown')
181
+ async def endpoint(request: KibaApiRequest[ExampleRequest]) -> ExampleResponse:
182
+ return ExampleResponse(result=request.data.value)
183
+
184
+
185
+ def test_route_without_auth_does_not_require_an_auth_resolver() -> None:
186
+ @api_route(requestType=ExampleRequest, responseType=ExampleResponse)
187
+ async def endpoint(request: KibaApiRequest[ExampleRequest]) -> ExampleResponse:
188
+ return ExampleResponse(result=request.data.value)
189
+
190
+ client = TestClient(Starlette(routes=[Route('/example', endpoint, methods=['POST'])]), raise_server_exceptions=False)
191
+ response = client.post('/example', json={'value': 'hello'})
192
+
193
+ assert response.status_code == 200
194
+ assert response.json() == {'result': 'hello'}
195
+
196
+
197
+ def test_route_rejects_auth_policy_without_auth_resolver_during_registration() -> None:
198
+ with pytest.raises(ValueError, match='Auth policy signature requires an authResolver'):
199
+
200
+ @api_route(requestType=ExampleRequest, responseType=ExampleResponse, auth='signature')
201
+ async def endpoint(request: KibaApiRequest[ExampleRequest]) -> ExampleResponse:
202
+ return ExampleResponse(result=request.data.value)
File without changes