clear-skies 1.19.22__py3-none-any.whl → 2.0.23__py3-none-any.whl

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 (362) hide show
  1. clear_skies-2.0.23.dist-info/METADATA +76 -0
  2. clear_skies-2.0.23.dist-info/RECORD +265 -0
  3. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info}/WHEEL +1 -1
  4. clearskies/__init__.py +37 -21
  5. clearskies/action.py +7 -0
  6. clearskies/authentication/__init__.py +9 -38
  7. clearskies/authentication/authentication.py +44 -0
  8. clearskies/authentication/authorization.py +14 -8
  9. clearskies/authentication/authorization_pass_through.py +22 -0
  10. clearskies/authentication/jwks.py +135 -58
  11. clearskies/authentication/public.py +3 -26
  12. clearskies/authentication/secret_bearer.py +515 -44
  13. clearskies/autodoc/formats/oai3_json/__init__.py +2 -2
  14. clearskies/autodoc/formats/oai3_json/oai3_json.py +11 -9
  15. clearskies/autodoc/formats/oai3_json/parameter.py +6 -3
  16. clearskies/autodoc/formats/oai3_json/request.py +7 -5
  17. clearskies/autodoc/formats/oai3_json/response.py +7 -4
  18. clearskies/autodoc/formats/oai3_json/schema/object.py +10 -1
  19. clearskies/autodoc/request/__init__.py +2 -0
  20. clearskies/autodoc/request/header.py +4 -6
  21. clearskies/autodoc/request/json_body.py +4 -6
  22. clearskies/autodoc/request/parameter.py +8 -0
  23. clearskies/autodoc/request/request.py +16 -4
  24. clearskies/autodoc/request/url_parameter.py +4 -6
  25. clearskies/autodoc/request/url_path.py +4 -6
  26. clearskies/autodoc/schema/__init__.py +4 -2
  27. clearskies/autodoc/schema/array.py +5 -6
  28. clearskies/autodoc/schema/boolean.py +4 -10
  29. clearskies/autodoc/schema/date.py +0 -3
  30. clearskies/autodoc/schema/datetime.py +1 -4
  31. clearskies/autodoc/schema/double.py +0 -3
  32. clearskies/autodoc/schema/enum.py +4 -2
  33. clearskies/autodoc/schema/integer.py +4 -9
  34. clearskies/autodoc/schema/long.py +0 -3
  35. clearskies/autodoc/schema/number.py +4 -9
  36. clearskies/autodoc/schema/object.py +5 -7
  37. clearskies/autodoc/schema/password.py +0 -3
  38. clearskies/autodoc/schema/schema.py +11 -0
  39. clearskies/autodoc/schema/string.py +4 -10
  40. clearskies/backends/__init__.py +56 -17
  41. clearskies/backends/api_backend.py +1128 -166
  42. clearskies/backends/backend.py +54 -85
  43. clearskies/backends/cursor_backend.py +246 -191
  44. clearskies/backends/memory_backend.py +514 -208
  45. clearskies/backends/secrets_backend.py +68 -31
  46. clearskies/column.py +1221 -0
  47. clearskies/columns/__init__.py +71 -0
  48. clearskies/columns/audit.py +306 -0
  49. clearskies/columns/belongs_to_id.py +478 -0
  50. clearskies/columns/belongs_to_model.py +129 -0
  51. clearskies/columns/belongs_to_self.py +109 -0
  52. clearskies/columns/boolean.py +110 -0
  53. clearskies/columns/category_tree.py +273 -0
  54. clearskies/columns/category_tree_ancestors.py +51 -0
  55. clearskies/columns/category_tree_children.py +126 -0
  56. clearskies/columns/category_tree_descendants.py +48 -0
  57. clearskies/columns/created.py +92 -0
  58. clearskies/columns/created_by_authorization_data.py +114 -0
  59. clearskies/columns/created_by_header.py +103 -0
  60. clearskies/columns/created_by_ip.py +90 -0
  61. clearskies/columns/created_by_routing_data.py +102 -0
  62. clearskies/columns/created_by_user_agent.py +89 -0
  63. clearskies/columns/date.py +232 -0
  64. clearskies/columns/datetime.py +284 -0
  65. clearskies/columns/email.py +78 -0
  66. clearskies/columns/float.py +149 -0
  67. clearskies/columns/has_many.py +529 -0
  68. clearskies/columns/has_many_self.py +62 -0
  69. clearskies/columns/has_one.py +21 -0
  70. clearskies/columns/integer.py +158 -0
  71. clearskies/columns/json.py +126 -0
  72. clearskies/columns/many_to_many_ids.py +335 -0
  73. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  74. clearskies/columns/many_to_many_models.py +156 -0
  75. clearskies/columns/many_to_many_pivots.py +132 -0
  76. clearskies/columns/phone.py +162 -0
  77. clearskies/columns/select.py +95 -0
  78. clearskies/columns/string.py +102 -0
  79. clearskies/columns/timestamp.py +164 -0
  80. clearskies/columns/updated.py +107 -0
  81. clearskies/columns/uuid.py +83 -0
  82. clearskies/configs/README.md +105 -0
  83. clearskies/configs/__init__.py +170 -0
  84. clearskies/configs/actions.py +43 -0
  85. clearskies/configs/any.py +15 -0
  86. clearskies/configs/any_dict.py +24 -0
  87. clearskies/configs/any_dict_or_callable.py +25 -0
  88. clearskies/configs/authentication.py +23 -0
  89. clearskies/configs/authorization.py +23 -0
  90. clearskies/configs/boolean.py +18 -0
  91. clearskies/configs/boolean_or_callable.py +20 -0
  92. clearskies/configs/callable_config.py +20 -0
  93. clearskies/configs/columns.py +34 -0
  94. clearskies/configs/conditions.py +30 -0
  95. clearskies/configs/config.py +26 -0
  96. clearskies/configs/datetime.py +20 -0
  97. clearskies/configs/datetime_or_callable.py +21 -0
  98. clearskies/configs/email.py +10 -0
  99. clearskies/configs/email_list.py +17 -0
  100. clearskies/configs/email_list_or_callable.py +17 -0
  101. clearskies/configs/email_or_email_list_or_callable.py +59 -0
  102. clearskies/configs/endpoint.py +23 -0
  103. clearskies/configs/endpoint_list.py +29 -0
  104. clearskies/configs/float.py +18 -0
  105. clearskies/configs/float_or_callable.py +20 -0
  106. clearskies/configs/headers.py +28 -0
  107. clearskies/configs/integer.py +18 -0
  108. clearskies/configs/integer_or_callable.py +20 -0
  109. clearskies/configs/joins.py +30 -0
  110. clearskies/configs/list_any_dict.py +32 -0
  111. clearskies/configs/list_any_dict_or_callable.py +33 -0
  112. clearskies/configs/model_class.py +35 -0
  113. clearskies/configs/model_column.py +67 -0
  114. clearskies/configs/model_columns.py +58 -0
  115. clearskies/configs/model_destination_name.py +26 -0
  116. clearskies/configs/model_to_id_column.py +45 -0
  117. clearskies/configs/readable_model_column.py +11 -0
  118. clearskies/configs/readable_model_columns.py +11 -0
  119. clearskies/configs/schema.py +23 -0
  120. clearskies/configs/searchable_model_columns.py +11 -0
  121. clearskies/configs/security_headers.py +39 -0
  122. clearskies/configs/select.py +28 -0
  123. clearskies/configs/select_list.py +49 -0
  124. clearskies/configs/string.py +31 -0
  125. clearskies/configs/string_dict.py +34 -0
  126. clearskies/configs/string_list.py +47 -0
  127. clearskies/configs/string_list_or_callable.py +48 -0
  128. clearskies/configs/string_or_callable.py +18 -0
  129. clearskies/configs/timedelta.py +20 -0
  130. clearskies/configs/timezone.py +20 -0
  131. clearskies/configs/url.py +25 -0
  132. clearskies/configs/validators.py +45 -0
  133. clearskies/configs/writeable_model_column.py +11 -0
  134. clearskies/configs/writeable_model_columns.py +11 -0
  135. clearskies/configurable.py +78 -0
  136. clearskies/contexts/__init__.py +8 -8
  137. clearskies/contexts/cli.py +129 -43
  138. clearskies/contexts/context.py +93 -56
  139. clearskies/contexts/wsgi.py +79 -33
  140. clearskies/contexts/wsgi_ref.py +87 -0
  141. clearskies/cursors/__init__.py +7 -0
  142. clearskies/cursors/cursor.py +166 -0
  143. clearskies/cursors/from_environment/__init__.py +5 -0
  144. clearskies/cursors/from_environment/mysql.py +51 -0
  145. clearskies/cursors/from_environment/postgresql.py +49 -0
  146. clearskies/cursors/from_environment/sqlite.py +35 -0
  147. clearskies/cursors/mysql.py +61 -0
  148. clearskies/cursors/postgresql.py +61 -0
  149. clearskies/cursors/sqlite.py +62 -0
  150. clearskies/decorators.py +33 -0
  151. clearskies/decorators.pyi +10 -0
  152. clearskies/di/__init__.py +11 -7
  153. clearskies/di/additional_config.py +117 -3
  154. clearskies/di/additional_config_auto_import.py +12 -0
  155. clearskies/di/di.py +717 -126
  156. clearskies/di/inject/__init__.py +23 -0
  157. clearskies/di/inject/akeyless_sdk.py +16 -0
  158. clearskies/di/inject/by_class.py +24 -0
  159. clearskies/di/inject/by_name.py +22 -0
  160. clearskies/di/inject/di.py +16 -0
  161. clearskies/di/inject/environment.py +15 -0
  162. clearskies/di/inject/input_output.py +19 -0
  163. clearskies/di/inject/now.py +16 -0
  164. clearskies/di/inject/requests.py +16 -0
  165. clearskies/di/inject/secrets.py +15 -0
  166. clearskies/di/inject/utcnow.py +16 -0
  167. clearskies/di/inject/uuid.py +16 -0
  168. clearskies/di/injectable.py +32 -0
  169. clearskies/di/injectable_properties.py +131 -0
  170. clearskies/end.py +219 -0
  171. clearskies/endpoint.py +1303 -0
  172. clearskies/endpoint_group.py +333 -0
  173. clearskies/endpoints/__init__.py +25 -0
  174. clearskies/endpoints/advanced_search.py +519 -0
  175. clearskies/endpoints/callable.py +382 -0
  176. clearskies/endpoints/create.py +201 -0
  177. clearskies/endpoints/delete.py +133 -0
  178. clearskies/endpoints/get.py +267 -0
  179. clearskies/endpoints/health_check.py +181 -0
  180. clearskies/endpoints/list.py +567 -0
  181. clearskies/endpoints/restful_api.py +417 -0
  182. clearskies/endpoints/schema.py +185 -0
  183. clearskies/endpoints/simple_search.py +279 -0
  184. clearskies/endpoints/update.py +188 -0
  185. clearskies/environment.py +7 -3
  186. clearskies/exceptions/__init__.py +19 -0
  187. clearskies/{handlers/exceptions/input_error.py → exceptions/input_errors.py} +1 -1
  188. clearskies/exceptions/missing_dependency.py +2 -0
  189. clearskies/exceptions/moved_permanently.py +3 -0
  190. clearskies/exceptions/moved_temporarily.py +3 -0
  191. clearskies/functional/__init__.py +2 -2
  192. clearskies/functional/json.py +47 -0
  193. clearskies/functional/routing.py +92 -0
  194. clearskies/functional/string.py +19 -11
  195. clearskies/functional/validations.py +61 -9
  196. clearskies/input_outputs/__init__.py +9 -7
  197. clearskies/input_outputs/cli.py +135 -152
  198. clearskies/input_outputs/exceptions/__init__.py +6 -1
  199. clearskies/input_outputs/headers.py +54 -0
  200. clearskies/input_outputs/input_output.py +77 -123
  201. clearskies/input_outputs/programmatic.py +62 -0
  202. clearskies/input_outputs/wsgi.py +36 -48
  203. clearskies/model.py +1894 -199
  204. clearskies/query/__init__.py +12 -0
  205. clearskies/query/condition.py +228 -0
  206. clearskies/query/join.py +136 -0
  207. clearskies/query/query.py +193 -0
  208. clearskies/query/sort.py +27 -0
  209. clearskies/schema.py +82 -0
  210. clearskies/secrets/__init__.py +4 -31
  211. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +15 -4
  212. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +11 -5
  213. clearskies/secrets/akeyless.py +421 -155
  214. clearskies/secrets/exceptions/__init__.py +7 -1
  215. clearskies/secrets/exceptions/not_found_error.py +2 -0
  216. clearskies/secrets/exceptions/permissions_error.py +2 -0
  217. clearskies/secrets/secrets.py +12 -11
  218. clearskies/security_header.py +17 -0
  219. clearskies/security_headers/__init__.py +8 -8
  220. clearskies/security_headers/cache_control.py +47 -109
  221. clearskies/security_headers/cors.py +38 -92
  222. clearskies/security_headers/csp.py +76 -150
  223. clearskies/security_headers/hsts.py +14 -15
  224. clearskies/typing.py +11 -0
  225. clearskies/validator.py +36 -0
  226. clearskies/validators/__init__.py +33 -0
  227. clearskies/validators/after_column.py +61 -0
  228. clearskies/validators/before_column.py +15 -0
  229. clearskies/validators/in_the_future.py +29 -0
  230. clearskies/validators/in_the_future_at_least.py +13 -0
  231. clearskies/validators/in_the_future_at_most.py +12 -0
  232. clearskies/validators/in_the_past.py +29 -0
  233. clearskies/validators/in_the_past_at_least.py +12 -0
  234. clearskies/validators/in_the_past_at_most.py +12 -0
  235. clearskies/validators/maximum_length.py +25 -0
  236. clearskies/validators/maximum_value.py +28 -0
  237. clearskies/validators/minimum_length.py +25 -0
  238. clearskies/validators/minimum_value.py +28 -0
  239. clearskies/{input_requirements → validators}/required.py +18 -9
  240. clearskies/validators/timedelta.py +58 -0
  241. clearskies/validators/unique.py +28 -0
  242. clear_skies-1.19.22.dist-info/METADATA +0 -46
  243. clear_skies-1.19.22.dist-info/RECORD +0 -206
  244. clearskies/application.py +0 -29
  245. clearskies/authentication/auth0_jwks.py +0 -118
  246. clearskies/authentication/auth_exception.py +0 -2
  247. clearskies/authentication/jwks_jwcrypto.py +0 -39
  248. clearskies/backends/example_backend.py +0 -43
  249. clearskies/backends/file_backend.py +0 -48
  250. clearskies/backends/json_backend.py +0 -7
  251. clearskies/backends/restful_api_advanced_search_backend.py +0 -138
  252. clearskies/binding_config.py +0 -16
  253. clearskies/column_types/__init__.py +0 -184
  254. clearskies/column_types/audit.py +0 -235
  255. clearskies/column_types/belongs_to.py +0 -250
  256. clearskies/column_types/boolean.py +0 -60
  257. clearskies/column_types/category_tree.py +0 -226
  258. clearskies/column_types/column.py +0 -373
  259. clearskies/column_types/created.py +0 -26
  260. clearskies/column_types/created_by_authorization_data.py +0 -26
  261. clearskies/column_types/created_by_header.py +0 -24
  262. clearskies/column_types/created_by_ip.py +0 -17
  263. clearskies/column_types/created_by_routing_data.py +0 -25
  264. clearskies/column_types/created_by_user_agent.py +0 -17
  265. clearskies/column_types/created_micro.py +0 -26
  266. clearskies/column_types/datetime.py +0 -108
  267. clearskies/column_types/datetime_micro.py +0 -12
  268. clearskies/column_types/email.py +0 -18
  269. clearskies/column_types/float.py +0 -43
  270. clearskies/column_types/has_many.py +0 -139
  271. clearskies/column_types/integer.py +0 -41
  272. clearskies/column_types/json.py +0 -25
  273. clearskies/column_types/many_to_many.py +0 -278
  274. clearskies/column_types/many_to_many_with_data.py +0 -162
  275. clearskies/column_types/select.py +0 -11
  276. clearskies/column_types/string.py +0 -24
  277. clearskies/column_types/updated.py +0 -24
  278. clearskies/column_types/updated_micro.py +0 -24
  279. clearskies/column_types/uuid.py +0 -25
  280. clearskies/columns.py +0 -123
  281. clearskies/condition_parser.py +0 -172
  282. clearskies/contexts/build_context.py +0 -54
  283. clearskies/contexts/convert_to_application.py +0 -190
  284. clearskies/contexts/extract_handler.py +0 -37
  285. clearskies/contexts/test.py +0 -94
  286. clearskies/decorators/__init__.py +0 -39
  287. clearskies/decorators/auth0_jwks.py +0 -22
  288. clearskies/decorators/authorization.py +0 -10
  289. clearskies/decorators/binding_classes.py +0 -9
  290. clearskies/decorators/binding_modules.py +0 -9
  291. clearskies/decorators/bindings.py +0 -9
  292. clearskies/decorators/create.py +0 -10
  293. clearskies/decorators/delete.py +0 -10
  294. clearskies/decorators/docs.py +0 -14
  295. clearskies/decorators/get.py +0 -10
  296. clearskies/decorators/jwks.py +0 -26
  297. clearskies/decorators/merge.py +0 -124
  298. clearskies/decorators/patch.py +0 -10
  299. clearskies/decorators/post.py +0 -10
  300. clearskies/decorators/public.py +0 -11
  301. clearskies/decorators/response_headers.py +0 -10
  302. clearskies/decorators/return_raw_response.py +0 -9
  303. clearskies/decorators/schema.py +0 -10
  304. clearskies/decorators/secret_bearer.py +0 -24
  305. clearskies/decorators/security_headers.py +0 -10
  306. clearskies/di/standard_dependencies.py +0 -140
  307. clearskies/di/test_module/__init__.py +0 -6
  308. clearskies/di/test_module/another_module/__init__.py +0 -2
  309. clearskies/di/test_module/module_class.py +0 -5
  310. clearskies/handlers/__init__.py +0 -41
  311. clearskies/handlers/advanced_search.py +0 -271
  312. clearskies/handlers/base.py +0 -473
  313. clearskies/handlers/callable.py +0 -189
  314. clearskies/handlers/create.py +0 -35
  315. clearskies/handlers/crud_by_method.py +0 -18
  316. clearskies/handlers/database_connector.py +0 -32
  317. clearskies/handlers/delete.py +0 -61
  318. clearskies/handlers/exceptions/__init__.py +0 -5
  319. clearskies/handlers/exceptions/not_found.py +0 -3
  320. clearskies/handlers/get.py +0 -156
  321. clearskies/handlers/health_check.py +0 -59
  322. clearskies/handlers/input_processing.py +0 -79
  323. clearskies/handlers/list.py +0 -530
  324. clearskies/handlers/mygrations.py +0 -82
  325. clearskies/handlers/request_method_routing.py +0 -47
  326. clearskies/handlers/restful_api.py +0 -218
  327. clearskies/handlers/routing.py +0 -62
  328. clearskies/handlers/schema_helper.py +0 -128
  329. clearskies/handlers/simple_routing.py +0 -204
  330. clearskies/handlers/simple_routing_route.py +0 -192
  331. clearskies/handlers/simple_search.py +0 -136
  332. clearskies/handlers/update.py +0 -96
  333. clearskies/handlers/write.py +0 -193
  334. clearskies/input_requirements/__init__.py +0 -68
  335. clearskies/input_requirements/after.py +0 -36
  336. clearskies/input_requirements/before.py +0 -36
  337. clearskies/input_requirements/in_the_future_at_least.py +0 -19
  338. clearskies/input_requirements/in_the_future_at_most.py +0 -19
  339. clearskies/input_requirements/in_the_past_at_least.py +0 -19
  340. clearskies/input_requirements/in_the_past_at_most.py +0 -19
  341. clearskies/input_requirements/maximum_length.py +0 -19
  342. clearskies/input_requirements/minimum_length.py +0 -22
  343. clearskies/input_requirements/requirement.py +0 -25
  344. clearskies/input_requirements/time_delta.py +0 -38
  345. clearskies/input_requirements/unique.py +0 -18
  346. clearskies/mocks/__init__.py +0 -7
  347. clearskies/mocks/input_output.py +0 -124
  348. clearskies/mocks/models.py +0 -142
  349. clearskies/models.py +0 -345
  350. clearskies/security_headers/base.py +0 -12
  351. clearskies/tests/simple_api/models/__init__.py +0 -2
  352. clearskies/tests/simple_api/models/status.py +0 -23
  353. clearskies/tests/simple_api/models/user.py +0 -21
  354. clearskies/tests/simple_api/users_api.py +0 -64
  355. {clear_skies-1.19.22.dist-info → clear_skies-2.0.23.dist-info/licenses}/LICENSE +0 -0
  356. /clearskies/{contexts/bash.py → autodoc/py.typed} +0 -0
  357. /clearskies/{handlers/exceptions → exceptions}/authentication.py +0 -0
  358. /clearskies/{handlers/exceptions → exceptions}/authorization.py +0 -0
  359. /clearskies/{handlers/exceptions → exceptions}/client_error.py +0 -0
  360. /clearskies/{secrets/exceptions → exceptions}/not_found.py +0 -0
  361. /clearskies/{tests/__init__.py → input_outputs/py.typed} +0 -0
  362. /clearskies/{tests/simple_api/__init__.py → py.typed} +0 -0
@@ -1,170 +1,371 @@
1
+ from __future__ import annotations
2
+
1
3
  import datetime
2
- from clearskies.di import AdditionalConfig
3
- from .exceptions import NotFound
4
-
5
-
6
- class AKeylessAdditionalConfig(AdditionalConfig):
7
- _allowed_auth_methods = ["aws_iam", "saml", "jwt", "access_key"]
8
- _auth_method = None
9
- _kwargs = None
10
-
11
- _auth_method_allowed_kwargs = {
12
- "aws_iam": [],
13
- "saml": ["profile"],
14
- "access_key": [],
15
- "jwt": ["jwt_env_key"],
16
- }
17
-
18
- _validate_kwargs = {
19
- "aws_iam": lambda kwargs: "",
20
- "saml": lambda kwargs: "",
21
- "access_key": lambda kwargs: "",
22
- "jwt": lambda kwargs: ""
23
- if "jwt_env_key" in kwargs
24
- else "Must provide 'jwt_env_key' with the name of the environment variable that contains the JWT when using akeyless_jwt_auth()",
25
- }
26
-
27
- def __init__(self, auth_method, **kwargs):
28
- if auth_method not in self._allowed_auth_methods:
29
- raise ValueError(
30
- f"Internal clearskies error: attempt to use unsupported akeyless auth method, {auth_method}"
31
- )
32
- self._auth_method = auth_method
33
- allowed_kwargs = set(["access_id", "api_host", *self._auth_method_allowed_kwargs[auth_method]])
34
- error = self._validate_kwargs[auth_method](kwargs)
35
- if error:
36
- raise ValueError(error)
37
- extra_keys = set(kwargs.keys()) - allowed_kwargs
38
- if len(extra_keys):
39
- raise ValueError(
40
- f"Unexpected keys were passed into akeyless_{auth_method}: "
41
- + ", ".join(extra_keys)
42
- + ". The expected keys are: "
43
- + ", ".join(allowed_kwargs)
44
- )
45
- self._kwargs = kwargs
46
-
47
- def provide_secrets(self, requests, environment):
48
- secrets = AKeyless(requests, environment)
49
- secrets.configure(access_type=self._auth_method, **self._kwargs)
50
- return secrets
51
-
52
-
53
- class AKeyless:
54
- _akeyless = None
55
- _access_id = None
56
- _access_type = None
57
- _api_host = None
58
- _token_refresh = None
59
- _token = None
60
- _environment = None
61
- _jwt_env_key = None
62
- _requests = None
63
- _api = None
64
-
65
- def __init__(self, requests, environment):
66
- self._requests = requests
67
- self._environment = environment
68
- import akeyless
69
-
70
- self._akeyless = akeyless
71
-
72
- def configure(self, access_id=None, access_type=None, jwt_env_key=None, api_host=None, profile=None):
73
- self._access_id = access_id if access_id is not None else self._environment.get("akeyless_access_id")
74
- self._access_type = access_type if access_type is not None else self._environment.get("akeyless_access_type")
75
- self._jwt_env_key = jwt_env_key
76
- self._api_host = api_host if api_host is not None else self._environment.get("akeyless_api_host", silent=True)
77
- self._profile = profile if profile is not None else "default"
78
-
79
- if not self._api_host:
80
- self._api_host = "https://api.akeyless.io"
81
-
82
- configuration = self._akeyless.Configuration(host=self._api_host)
83
- api_client = self._akeyless.ApiClient(configuration)
84
- self._api = self._akeyless.V2Api(api_client)
85
-
86
- def create(self, path, value):
87
- self._configure_guard()
88
- res = self._api.create_secret(self._akeyless.CreateSecret(name=path, value=str(value), token=self._get_token()))
4
+ import logging
5
+ from types import ModuleType
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from clearskies import configs, secrets
9
+ from clearskies.decorators import parameters_to_properties
10
+ from clearskies.di import inject
11
+ from clearskies.functional.json import get_nested_attribute
12
+ from clearskies.secrets.exceptions import PermissionsError
13
+
14
+ if TYPE_CHECKING:
15
+ from akeyless import ListItemsOutput, V2Api # type: ignore[import-untyped]
16
+
17
+
18
+ class Akeyless(secrets.Secrets):
19
+ """
20
+ Backend for managing secrets using the Akeyless Vault.
21
+
22
+ This class provides integration with Akeyless vault services, allowing you to store, retrieve,
23
+ and manage secrets. It supports different types of secrets (static, dynamic, rotated) and
24
+ includes authentication mechanisms for AWS IAM, SAML, and JWT.
25
+ """
26
+
27
+ """
28
+ HTTP client for making API requests
29
+ """
30
+ requests = inject.Requests()
31
+
32
+ """
33
+ Environment configuration for retrieving environment variables
34
+ """
35
+ environment = inject.Environment()
36
+
37
+ """
38
+ The Akeyless SDK module injected by the dependency injection system
39
+ """
40
+ akeyless: ModuleType = inject.ByName("akeyless_sdk") # type: ignore
41
+
42
+ """
43
+ The access ID for the Akeyless service
44
+
45
+ This must match the pattern p-[0-9a-zA-Z]+ (e.g., "p-abc123")
46
+ """
47
+ access_id = configs.String(required=True, regexp=r"^p-[\d\w]+$")
48
+
49
+ """
50
+ The authentication method to use
51
+
52
+ Must be one of "aws_iam", "saml", or "jwt"
53
+ """
54
+ access_type = configs.Select(["aws_iam", "saml", "jwt"], required=True)
55
+
56
+ """
57
+ The Akeyless API host to connect to
58
+
59
+ Defaults to "https://api.akeyless.io"
60
+ """
61
+ api_host = configs.String(default="https://api.akeyless.io")
62
+
63
+ """
64
+ The environment variable key that contains the JWT when using JWT authentication
65
+
66
+ This is required when access_type is "jwt"
67
+ """
68
+ jwt_env_key = configs.String(required=False)
69
+
70
+ """
71
+ The SAML profile name when using SAML authentication
72
+
73
+ Must match the pattern [0-9a-zA-Z-]+ if provided
74
+ """
75
+ profile = configs.String(regexp=r"^[\d\w-]+$", default="default")
76
+
77
+ """
78
+ Whether to automatically guess the secret type
79
+
80
+ When enabled, the system will check the secret type (static, dynamic, rotated)
81
+ and call the appropriate method to retrieve it.
82
+ """
83
+ auto_guess_type = configs.Boolean(default=False)
84
+
85
+ """
86
+ When the current token expires
87
+ """
88
+ _token_refresh: datetime.datetime # type: ignore
89
+
90
+ """
91
+ The current authentication token
92
+ """
93
+ _token: str
94
+
95
+ """
96
+ The configured V2Api client
97
+ """
98
+ _api: V2Api
99
+
100
+ @parameters_to_properties
101
+ def __init__(
102
+ self,
103
+ access_id: str,
104
+ access_type: str,
105
+ jwt_env_key: str | None = None,
106
+ api_host: str | None = None,
107
+ profile: str | None = None,
108
+ auto_guess_type: bool = False,
109
+ ):
110
+ """
111
+ Initialize the Akeyless backend with the specified configuration.
112
+
113
+ The access_id must be provided and follow the format p-[0-9a-zA-Z]+. The access_type must be
114
+ one of "aws_iam", "saml", or "jwt". If using JWT authentication, jwt_env_key must be provided.
115
+ """
116
+ self.finalize_and_validate_configuration()
117
+ self.logger = logging.getLogger(self.__class__.__name__)
118
+
119
+ def configure(self) -> None:
120
+ """
121
+ Perform additional configuration validation.
122
+
123
+ Ensures that when using JWT authentication, the jwt_env_key is provided. Raises ValueError
124
+ if access_type is "jwt" and jwt_env_key is not provided.
125
+ """
126
+ if self.access_type == "jwt" and not self.jwt_env_key:
127
+ raise ValueError("When using the JWT access type for Akeyless you must provide jwt_env_key")
128
+
129
+ @property
130
+ def api(self) -> V2Api:
131
+ """
132
+ Get the configured V2Api client.
133
+
134
+ Creates a new API client if one doesn't exist yet, using the configured api_host.
135
+ """
136
+ if not hasattr(self, "_api"):
137
+ configuration = self.akeyless.Configuration(host=self.api_host)
138
+ self._api = self.akeyless.V2Api(self.akeyless.ApiClient(configuration))
139
+ return self._api
140
+
141
+ def create(self, path: str, value: Any) -> bool:
142
+ """
143
+ Create a new secret at the given path.
144
+
145
+ Checks permissions before creating the secret and raises PermissionsError if the user doesn't
146
+ have write permission for the path. The value is converted to a string before storage.
147
+ """
148
+ if not "write" in self.describe_permissions(path):
149
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
150
+
151
+ res = self.api.create_secret(self.akeyless.CreateSecret(name=path, value=str(value), token=self._get_token()))
89
152
  return True
90
153
 
91
- def get(self, path, silent_if_not_found=False):
92
- self._configure_guard()
154
+ def get(
155
+ self,
156
+ path: str,
157
+ silent_if_not_found: bool = False,
158
+ json_attribute: str | None = None,
159
+ args: dict[str, Any] | None = None,
160
+ ) -> str:
161
+ """
162
+ Get the secret at the given path.
163
+
164
+ When auto_guess_type is enabled, this method automatically determines if the secret is static,
165
+ dynamic, or rotated and calls the appropriate method to retrieve it. If silent_if_not_found is
166
+ True, returns an empty string when the secret is not found. If json_attribute is provided,
167
+ treats the secret as JSON and returns the specified attribute.
168
+ """
169
+ if not self.auto_guess_type:
170
+ return self.get_static_secret(path, silent_if_not_found=silent_if_not_found, json_attribute=json_attribute)
93
171
 
94
172
  try:
95
- res = self._api.get_secret_value(self._akeyless.GetSecretValue(names=[path], token=self._get_token()))
173
+ secret = self.describe_secret(path)
96
174
  except Exception as e:
97
- if e.status == 404:
175
+ if e.status == 404: # type: ignore
98
176
  if silent_if_not_found:
99
- return None
100
- raise NotFound(f"Secret '{path}' not found")
101
- raise e
102
- return res[path]
177
+ return ""
178
+ raise e
179
+ else:
180
+ raise ValueError(
181
+ f"describe-secret call failed for path {path}: perhaps a permissions issue? Akeless says {e}"
182
+ )
183
+
184
+ self.logger.debug(f"Auto-detected secret type '{secret.item_type}' for secret '{path}'")
185
+ match secret.item_type.lower():
186
+ case "dynamic_secret":
187
+ return str(
188
+ self.get_dynamic_secret(
189
+ path,
190
+ json_attribute=json_attribute,
191
+ args=args,
192
+ )
193
+ )
194
+ case "rotated_secret":
195
+ return str(self.get_rotated_secret(path, json_attribute=json_attribute, args=args))
196
+ case "static_secret":
197
+ return self.get_static_secret(
198
+ path, json_attribute=json_attribute, silent_if_not_found=silent_if_not_found
199
+ )
200
+ case _:
201
+ raise ValueError(f"Unsupported secret type for auto-detection: '{secret.item_type}'")
202
+
203
+ def get_static_secret(self, path: str, silent_if_not_found: bool = False, json_attribute: str | None = None) -> str:
204
+ """
205
+ Get a static secret from the given path.
206
+
207
+ Checks permissions before retrieving the secret and raises PermissionsError if the user doesn't
208
+ have read permission. If silent_if_not_found is True, returns an empty string when the secret
209
+ is not found. If json_attribute is provided, treats the secret as JSON and returns the specified attribute.
210
+ """
211
+ if not "read" in self.describe_permissions(path):
212
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
103
213
 
104
- def get_dynamic_secret(self, path, args=None):
105
- self._configure_guard()
214
+ try:
215
+ res: dict[str, object] = self.api.get_secret_value( # type: ignore
216
+ self.akeyless.GetSecretValue(
217
+ names=[path], token=self._get_token(), json=True if json_attribute else False
218
+ )
219
+ )
220
+ except Exception as e:
221
+ if e.status == 404: # type: ignore
222
+ if silent_if_not_found:
223
+ return ""
224
+ raise KeyError(f"Secret '{path}' not found")
225
+ raise e
226
+ if json_attribute:
227
+ return get_nested_attribute(res[path], json_attribute) # type: ignore
228
+ return str(res[path])
229
+
230
+ def get_dynamic_secret(
231
+ self, path: str, json_attribute: str | None = None, args: dict[str, Any] | None = None
232
+ ) -> Any:
233
+ """
234
+ Get a dynamic secret from the given path.
235
+
236
+ Dynamic secrets are generated on-demand, such as database credentials. Checks permissions
237
+ before retrieving the secret and raises PermissionsError if the user doesn't have read
238
+ permission. If json_attribute is provided, treats the result as JSON and returns the
239
+ specified attribute.
240
+ """
241
+ if not "read" in self.describe_permissions(path):
242
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
106
243
 
107
244
  kwargs = {
108
245
  "name": path,
109
246
  "token": self._get_token(),
110
247
  }
111
248
  if args:
112
- kwargs["args"] = args
113
-
114
- res = self._api.get_dynamic_secret_value(self._akeyless.GetDynamicSecretValue(**kwargs))
249
+ kwargs["args"] = args # type: ignore
250
+ res: dict[str, Any] = self.api.get_dynamic_secret_value(self.akeyless.GetDynamicSecretValue(**kwargs)) # type: ignore
251
+ if json_attribute:
252
+ return get_nested_attribute(res, json_attribute)
115
253
  return res
116
254
 
117
- def get_rotated_secret(self, path, args=None):
118
- self._configure_guard()
255
+ def get_rotated_secret(
256
+ self, path: str, json_attribute: str | None = None, args: dict[str, Any] | None = None
257
+ ) -> Any:
258
+ """
259
+ Get a rotated secret from the given path.
260
+
261
+ Rotated secrets are automatically replaced on a schedule. Checks permissions before
262
+ retrieving the secret and raises PermissionsError if the user doesn't have read
263
+ permission. If json_attribute is provided, treats the result as JSON and returns the
264
+ specified attribute.
265
+ """
266
+ if not "read" in self.describe_permissions(path):
267
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
119
268
 
120
269
  kwargs = {
121
270
  "names": path,
122
271
  "token": self._get_token(),
272
+ "json": True if json_attribute else False,
123
273
  }
124
274
  if args:
125
- kwargs["args"] = args
275
+ kwargs["args"] = args # type: ignore
126
276
 
127
- res = self._api.get_rotated_secret_value(self._akeyless.GetRotatedSecretValue(**kwargs))
277
+ res: dict[str, str] = self._api.get_rotated_secret_value(self.akeyless.GetRotatedSecretValue(**kwargs))["value"] # type: ignore
278
+ if json_attribute:
279
+ return get_nested_attribute(res, json_attribute)
128
280
  return res
129
281
 
130
- def list_secrets(self, path):
131
- self._configure_guard()
132
- res = self._api.list_items(self._akeyless.ListItems(path=path, token=self._get_token()))
282
+ def describe_secret(self, path: str) -> Any:
283
+ """
284
+ Get metadata about a secret.
285
+
286
+ Checks permissions before retrieving metadata and raises PermissionsError if the user
287
+ doesn't have read permission for the path.
288
+ """
289
+ if not "read" in self.describe_permissions(path):
290
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
291
+
292
+ return self.api.describe_item(self.akeyless.DescribeItem(name=path, token=self._get_token()))
293
+
294
+ def list_secrets(self, path: str) -> list[Any]:
295
+ """
296
+ List all secrets at the given path.
297
+
298
+ Checks permissions before listing secrets and raises PermissionsError if the user doesn't
299
+ have list permission for the path. Returns an empty list if no secrets are found.
300
+ """
301
+ if not "list" in self.describe_permissions(path):
302
+ raise PermissionsError(f"You do not have permission the secrets in '{path}'")
303
+
304
+ res: ListItemsOutput = self.api.list_items( # type: ignore
305
+ self.akeyless.ListItems(
306
+ path=path,
307
+ token=self._get_token(),
308
+ )
309
+ )
133
310
  if not res.items:
134
311
  return []
135
312
 
136
313
  return [item.item_name for item in res.items]
137
314
 
138
- def update(self, path, value):
139
- self._configure_guard()
140
- res = self._api.update_secret_val(
141
- self._akeyless.UpdateSecretVal(name=path, value=str(value), token=self._get_token())
315
+ def update(self, path: str, value: Any) -> None:
316
+ """
317
+ Update an existing secret.
318
+
319
+ Checks permissions before updating the secret and raises PermissionsError if the user
320
+ doesn't have write permission for the path. The value is converted to a string before storage.
321
+ """
322
+ if not "write" in self.describe_permissions(path):
323
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
324
+
325
+ res = self.api.update_secret_val(
326
+ self.akeyless.UpdateSecretVal(name=path, value=str(value), token=self._get_token())
142
327
  )
143
- return True
144
328
 
145
- def upsert(self, path, value):
329
+ def upsert(self, path: str, value: Any) -> None:
330
+ """
331
+ Create or update a secret.
332
+
333
+ This method attempts to update an existing secret, and if that fails, it tries to create
334
+ a new one. The value is converted to a string before storage.
335
+ """
146
336
  try:
147
- if self.update(path, value):
148
- return True
337
+ self.update(path, value)
149
338
  except Exception as e:
150
- return self.create(path, value)
339
+ self.create(path, value)
151
340
 
152
341
  def list_sub_folders(self, main_folder: str) -> list[str]:
153
- """Return the list of secrets/sub folders in the given folder."""
154
- items = self._api.list_items(self._akeyless.ListItems(path=main_folder, token=self._get_token()))
342
+ """
343
+ Return the list of secrets/sub folders in the given folder.
344
+
345
+ Checks permissions before listing subfolders and raises PermissionsError if the user doesn't
346
+ have list permission for the path. Returns the relative subfolder names without the parent path.
347
+ """
348
+ if not "list" in self.describe_permissions(main_folder):
349
+ raise PermissionsError(f"You do not have permission to list sub folders in '{main_folder}'")
350
+
351
+ items = self.api.list_items(self.akeyless.ListItems(path=main_folder, token=self._get_token()))
155
352
 
156
353
  # akeyless will return the absolute path and end in a slash but we only want the folder name
157
354
  main_folder_string_len = len(main_folder)
158
- return [sub_folder[main_folder_string_len:-1] for sub_folder in items.folders]
355
+ return [sub_folder[main_folder_string_len:-1] for sub_folder in items.folders] # type: ignore
159
356
 
160
- def get_ssh_certificate(self, cert_issuer, cert_username, path_to_public_file):
161
- self._configure_guard()
357
+ def get_ssh_certificate(self, cert_issuer: str, cert_username: str, path_to_public_file: str) -> Any:
358
+ """
359
+ Get an SSH certificate from Akeyless.
162
360
 
361
+ Reads the public key from the specified file path and requests a certificate for the given
362
+ username and issuer from Akeyless.
363
+ """
163
364
  with open(path_to_public_file, "r") as fp:
164
365
  public_key = fp.read()
165
366
 
166
- res = self._api.get_ssh_certificate(
167
- self._akeyless.GetSSHCertificate(
367
+ res = self.api.get_ssh_certificate(
368
+ self.akeyless.GetSSHCertificate(
168
369
  cert_username=cert_username,
169
370
  cert_issuer_name=cert_issuer,
170
371
  public_key_data=public_key,
@@ -172,70 +373,135 @@ class AKeyless:
172
373
  )
173
374
  )
174
375
 
175
- return res.data
376
+ return res.data # type: ignore
176
377
 
177
- def _configure_guard(self):
178
- if not self._access_id:
179
- raise ValueError("Must call configure method before using secrets.AKeyless")
378
+ def _get_token(self) -> str:
379
+ """
380
+ Get an authentication token for Akeyless API calls.
180
381
 
181
- def _get_token(self):
382
+ Returns a cached token if available and not expired (within 10 seconds), otherwise obtains
383
+ a new one using the configured authentication method. Tokens are valid for about an hour,
384
+ but we set the refresh time to 30 minutes to be safe.
385
+ """
182
386
  # AKeyless tokens live for an hour
183
- if self._token is not None and (self._token_refresh - datetime.datetime.now()).total_seconds() > 10:
387
+ if (
388
+ hasattr(self, "_token_refresh")
389
+ and hasattr(self, "_token")
390
+ and (self._token_refresh - datetime.datetime.now()).total_seconds() > 10
391
+ ):
184
392
  return self._token
185
393
 
186
- auth_method_name = f"auth_{self._access_type}"
394
+ auth_method_name = f"auth_{self.access_type}"
187
395
  if not hasattr(self, auth_method_name):
188
- raise ValueError(f"Requested AKeyless authentication with unsupported auth method: '{self._access_type}'")
396
+ raise ValueError(f"Requested Akeyless authentication with unsupported auth method: '{self.access_type}'")
189
397
 
190
398
  self._token_refresh = datetime.datetime.now() + datetime.timedelta(hours=0.5)
191
399
  self._token = getattr(self, auth_method_name)()
192
400
  return self._token
193
401
 
194
402
  def auth_aws_iam(self):
195
- from akeyless_cloud_id import CloudId
403
+ """
404
+ Authenticate using AWS IAM.
405
+
406
+ Uses the akeyless_cloud_id package to generate a cloud ID and authenticates with Akeyless
407
+ using the configured access_id.
408
+ """
409
+ from akeyless_cloud_id import CloudId # type: ignore
196
410
 
197
- res = self._api.auth(
198
- self._akeyless.Auth(access_id=self._access_id, access_type="aws_iam", cloud_id=CloudId().generate())
411
+ res = self.api.auth(
412
+ self.akeyless.Auth(access_id=self.access_id, access_type="aws_iam", cloud_id=CloudId().generate())
199
413
  )
200
- return res.token
414
+ return res.token # type: ignore
201
415
 
202
416
  def auth_saml(self):
417
+ """
418
+ Authenticate using SAML.
419
+
420
+ Uses the akeyless CLI to generate credentials and then retrieves a token either directly
421
+ from the credentials file or by making an API call to convert the credentials to a token.
422
+ """
423
+ import json
203
424
  import os
204
425
  from pathlib import Path
205
426
 
206
- os.system(f"akeyless list-items --profile {self._profile} --path /not/a/real/path > /dev/null 2>&1")
427
+ os.system(f"akeyless list-items --profile {self.profile} --path /not/a/real/path > /dev/null 2>&1")
207
428
  home = str(Path.home())
208
- with open(f"{home}/.akeyless/.tmp_creds/{self._profile}-{self._access_id}", "r") as creds_file:
429
+ with open(f"{home}/.akeyless/.tmp_creds/{self.profile}-{self.access_id}", "r") as creds_file:
209
430
  credentials = creds_file.read()
210
-
431
+ credentials_json = json.loads(credentials)
432
+ if "token" in credentials_json:
433
+ return credentials_json["token"]
211
434
  # and now we can turn that into a token
212
- response = self._requests.post(
435
+ response = self.requests.post(
213
436
  "https://rest.akeyless.io/",
214
437
  data={
215
438
  "cmd": "static-creds-auth",
216
- "access-id": self._access_id,
439
+ "access-id": self.access_id,
217
440
  "creds": credentials.strip(),
218
441
  },
219
442
  )
220
443
  return response.json()["token"]
221
444
 
222
445
  def auth_jwt(self):
223
- if not self._jwt_env_key:
446
+ """
447
+ Authenticate using JWT.
448
+
449
+ Retrieves the JWT from the environment variable specified by jwt_env_key and authenticates
450
+ with Akeyless. Raises ValueError if jwt_env_key is not specified.
451
+ """
452
+ if not self.jwt_env_key:
224
453
  raise ValueError(
225
- "To use AKeyless JWT Auth, you must specify the name of the ENV key to load the JWT from when configuring AKeyless"
226
- )
227
- res = self._api.auth(
228
- self._akeyless.Auth(
229
- access_id=self._access_id, access_type="jwt", jwt=self._environment.get(self._jwt_env_key)
454
+ "To use AKeyless JWT Auth, "
455
+ "you must specify the name of the ENV key to load the JWT from when configuring AKeyless"
230
456
  )
457
+ res = self.api.auth(
458
+ self.akeyless.Auth(access_id=self.access_id, access_type="jwt", jwt=self.environment.get(self.jwt_env_key))
231
459
  )
232
- return res.token
460
+ return res.token # type: ignore
233
461
 
234
- def auth_access_key(self):
235
- access_key = self._environment.get("akeyless_access_key", silent=True)
236
- if not access_key:
237
- print(
238
- "To use AKeyless access key auth, you must specify your AKeyless access key in the 'akeyless_access_key' environment variable"
239
- )
240
- res = self._api.auth(self._akeyless.Auth(access_id=self._access_id, access_key=access_key))
241
- return res.token
462
+ def describe_permissions(self, path: str, type: str = "item") -> list[str]:
463
+ """
464
+ List permissions for a path.
465
+
466
+ Returns a list of permission strings (e.g., "read", "write", "list") that the current
467
+ authentication token has for the specified path.
468
+ """
469
+ return self.api.describe_permissions(
470
+ self.akeyless.DescribePermissions(token=self._get_token(), path=path, type=type)
471
+ ).client_permissions # type: ignore
472
+
473
+
474
+ class AkeylessSaml(Akeyless):
475
+ """Convenience class for SAML authentication with Akeyless."""
476
+
477
+ def __init__(self, access_id: str, api_host: str = "", profile: str = ""):
478
+ """
479
+ Initialize with SAML authentication.
480
+
481
+ Sets access_type to "saml" and passes the remaining parameters to the parent class.
482
+ """
483
+ return super().__init__(access_id, "saml", api_host=api_host, profile=profile)
484
+
485
+
486
+ class AkeylessJwt(Akeyless):
487
+ """Convenience class for JWT authentication with Akeyless."""
488
+
489
+ def __init__(self, access_id: str, jwt_env_key: str = "", api_host: str = "", profile: str = ""):
490
+ """
491
+ Initialize with JWT authentication.
492
+
493
+ Sets access_type to "jwt" and passes the remaining parameters to the parent class.
494
+ """
495
+ return super().__init__(access_id, "jwt", jwt_env_key=jwt_env_key, api_host=api_host, profile=profile)
496
+
497
+
498
+ class AkeylessAwsIam(Akeyless):
499
+ """Convenience class for AWS IAM authentication with Akeyless."""
500
+
501
+ def __init__(self, access_id: str, api_host: str = ""):
502
+ """
503
+ Initialize with AWS IAM authentication.
504
+
505
+ Sets access_type to "aws_iam" and passes the remaining parameters to the parent class.
506
+ """
507
+ return super().__init__(access_id, "aws_iam", api_host=api_host)