clear-skies 2.0.5__py3-none-any.whl → 2.0.7__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.

Potentially problematic release.


This version of clear-skies might be problematic. Click here for more details.

Files changed (252) hide show
  1. {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/METADATA +1 -1
  2. clear_skies-2.0.7.dist-info/RECORD +251 -0
  3. clearskies/__init__.py +61 -0
  4. clearskies/action.py +7 -0
  5. clearskies/authentication/__init__.py +15 -0
  6. clearskies/authentication/authentication.py +46 -0
  7. clearskies/authentication/authorization.py +16 -0
  8. clearskies/authentication/authorization_pass_through.py +20 -0
  9. clearskies/authentication/jwks.py +163 -0
  10. clearskies/authentication/public.py +5 -0
  11. clearskies/authentication/secret_bearer.py +553 -0
  12. clearskies/autodoc/__init__.py +8 -0
  13. clearskies/autodoc/formats/__init__.py +5 -0
  14. clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
  15. clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
  16. clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
  17. clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
  18. clearskies/autodoc/formats/oai3_json/request.py +68 -0
  19. clearskies/autodoc/formats/oai3_json/response.py +28 -0
  20. clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
  21. clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
  22. clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
  23. clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
  24. clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
  25. clearskies/autodoc/formats/oai3_json/test.json +1985 -0
  26. clearskies/autodoc/py.typed +0 -0
  27. clearskies/autodoc/request/__init__.py +15 -0
  28. clearskies/autodoc/request/header.py +6 -0
  29. clearskies/autodoc/request/json_body.py +6 -0
  30. clearskies/autodoc/request/parameter.py +8 -0
  31. clearskies/autodoc/request/request.py +47 -0
  32. clearskies/autodoc/request/url_parameter.py +6 -0
  33. clearskies/autodoc/request/url_path.py +6 -0
  34. clearskies/autodoc/response/__init__.py +5 -0
  35. clearskies/autodoc/response/response.py +9 -0
  36. clearskies/autodoc/schema/__init__.py +31 -0
  37. clearskies/autodoc/schema/array.py +10 -0
  38. clearskies/autodoc/schema/base64.py +8 -0
  39. clearskies/autodoc/schema/boolean.py +5 -0
  40. clearskies/autodoc/schema/date.py +5 -0
  41. clearskies/autodoc/schema/datetime.py +5 -0
  42. clearskies/autodoc/schema/double.py +5 -0
  43. clearskies/autodoc/schema/enum.py +17 -0
  44. clearskies/autodoc/schema/integer.py +6 -0
  45. clearskies/autodoc/schema/long.py +5 -0
  46. clearskies/autodoc/schema/number.py +6 -0
  47. clearskies/autodoc/schema/object.py +13 -0
  48. clearskies/autodoc/schema/password.py +5 -0
  49. clearskies/autodoc/schema/schema.py +11 -0
  50. clearskies/autodoc/schema/string.py +5 -0
  51. clearskies/backends/__init__.py +65 -0
  52. clearskies/backends/api_backend.py +1178 -0
  53. clearskies/backends/backend.py +136 -0
  54. clearskies/backends/cursor_backend.py +335 -0
  55. clearskies/backends/memory_backend.py +797 -0
  56. clearskies/backends/secrets_backend.py +106 -0
  57. clearskies/column.py +1233 -0
  58. clearskies/columns/__init__.py +71 -0
  59. clearskies/columns/audit.py +206 -0
  60. clearskies/columns/belongs_to_id.py +483 -0
  61. clearskies/columns/belongs_to_model.py +132 -0
  62. clearskies/columns/belongs_to_self.py +105 -0
  63. clearskies/columns/boolean.py +113 -0
  64. clearskies/columns/category_tree.py +275 -0
  65. clearskies/columns/category_tree_ancestors.py +51 -0
  66. clearskies/columns/category_tree_children.py +127 -0
  67. clearskies/columns/category_tree_descendants.py +48 -0
  68. clearskies/columns/created.py +95 -0
  69. clearskies/columns/created_by_authorization_data.py +116 -0
  70. clearskies/columns/created_by_header.py +99 -0
  71. clearskies/columns/created_by_ip.py +92 -0
  72. clearskies/columns/created_by_routing_data.py +97 -0
  73. clearskies/columns/created_by_user_agent.py +92 -0
  74. clearskies/columns/date.py +234 -0
  75. clearskies/columns/datetime.py +282 -0
  76. clearskies/columns/email.py +76 -0
  77. clearskies/columns/float.py +153 -0
  78. clearskies/columns/has_many.py +505 -0
  79. clearskies/columns/has_many_self.py +56 -0
  80. clearskies/columns/has_one.py +14 -0
  81. clearskies/columns/integer.py +160 -0
  82. clearskies/columns/json.py +128 -0
  83. clearskies/columns/many_to_many_ids.py +337 -0
  84. clearskies/columns/many_to_many_ids_with_data.py +274 -0
  85. clearskies/columns/many_to_many_models.py +158 -0
  86. clearskies/columns/many_to_many_pivots.py +134 -0
  87. clearskies/columns/phone.py +159 -0
  88. clearskies/columns/select.py +92 -0
  89. clearskies/columns/string.py +102 -0
  90. clearskies/columns/timestamp.py +164 -0
  91. clearskies/columns/updated.py +110 -0
  92. clearskies/columns/uuid.py +86 -0
  93. clearskies/configs/README.md +105 -0
  94. clearskies/configs/__init__.py +162 -0
  95. clearskies/configs/actions.py +43 -0
  96. clearskies/configs/any.py +13 -0
  97. clearskies/configs/any_dict.py +22 -0
  98. clearskies/configs/any_dict_or_callable.py +23 -0
  99. clearskies/configs/authentication.py +23 -0
  100. clearskies/configs/authorization.py +23 -0
  101. clearskies/configs/boolean.py +16 -0
  102. clearskies/configs/boolean_or_callable.py +18 -0
  103. clearskies/configs/callable_config.py +18 -0
  104. clearskies/configs/columns.py +34 -0
  105. clearskies/configs/conditions.py +30 -0
  106. clearskies/configs/config.py +24 -0
  107. clearskies/configs/datetime.py +18 -0
  108. clearskies/configs/datetime_or_callable.py +19 -0
  109. clearskies/configs/endpoint.py +23 -0
  110. clearskies/configs/endpoint_list.py +29 -0
  111. clearskies/configs/float.py +16 -0
  112. clearskies/configs/float_or_callable.py +18 -0
  113. clearskies/configs/integer.py +16 -0
  114. clearskies/configs/integer_or_callable.py +18 -0
  115. clearskies/configs/joins.py +30 -0
  116. clearskies/configs/list_any_dict.py +30 -0
  117. clearskies/configs/list_any_dict_or_callable.py +31 -0
  118. clearskies/configs/model_class.py +35 -0
  119. clearskies/configs/model_column.py +65 -0
  120. clearskies/configs/model_columns.py +56 -0
  121. clearskies/configs/model_destination_name.py +25 -0
  122. clearskies/configs/model_to_id_column.py +43 -0
  123. clearskies/configs/readable_model_column.py +9 -0
  124. clearskies/configs/readable_model_columns.py +9 -0
  125. clearskies/configs/schema.py +23 -0
  126. clearskies/configs/searchable_model_columns.py +9 -0
  127. clearskies/configs/security_headers.py +39 -0
  128. clearskies/configs/select.py +26 -0
  129. clearskies/configs/select_list.py +47 -0
  130. clearskies/configs/string.py +29 -0
  131. clearskies/configs/string_dict.py +32 -0
  132. clearskies/configs/string_list.py +32 -0
  133. clearskies/configs/string_list_or_callable.py +35 -0
  134. clearskies/configs/string_or_callable.py +18 -0
  135. clearskies/configs/timedelta.py +18 -0
  136. clearskies/configs/timezone.py +18 -0
  137. clearskies/configs/url.py +23 -0
  138. clearskies/configs/validators.py +45 -0
  139. clearskies/configs/writeable_model_column.py +9 -0
  140. clearskies/configs/writeable_model_columns.py +9 -0
  141. clearskies/configurable.py +76 -0
  142. clearskies/contexts/__init__.py +11 -0
  143. clearskies/contexts/cli.py +117 -0
  144. clearskies/contexts/context.py +98 -0
  145. clearskies/contexts/wsgi.py +76 -0
  146. clearskies/contexts/wsgi_ref.py +82 -0
  147. clearskies/decorators.py +33 -0
  148. clearskies/di/__init__.py +15 -0
  149. clearskies/di/additional_config.py +130 -0
  150. clearskies/di/additional_config_auto_import.py +17 -0
  151. clearskies/di/di.py +973 -0
  152. clearskies/di/inject/__init__.py +23 -0
  153. clearskies/di/inject/by_class.py +21 -0
  154. clearskies/di/inject/by_name.py +18 -0
  155. clearskies/di/inject/di.py +13 -0
  156. clearskies/di/inject/environment.py +14 -0
  157. clearskies/di/inject/input_output.py +20 -0
  158. clearskies/di/inject/now.py +13 -0
  159. clearskies/di/inject/requests.py +13 -0
  160. clearskies/di/inject/secrets.py +14 -0
  161. clearskies/di/inject/utcnow.py +13 -0
  162. clearskies/di/inject/uuid.py +15 -0
  163. clearskies/di/injectable.py +29 -0
  164. clearskies/di/injectable_properties.py +131 -0
  165. clearskies/di/test_module/__init__.py +6 -0
  166. clearskies/di/test_module/another_module/__init__.py +2 -0
  167. clearskies/di/test_module/module_class.py +5 -0
  168. clearskies/end.py +183 -0
  169. clearskies/endpoint.py +1314 -0
  170. clearskies/endpoint_group.py +336 -0
  171. clearskies/endpoints/__init__.py +25 -0
  172. clearskies/endpoints/advanced_search.py +526 -0
  173. clearskies/endpoints/callable.py +388 -0
  174. clearskies/endpoints/create.py +205 -0
  175. clearskies/endpoints/delete.py +139 -0
  176. clearskies/endpoints/get.py +271 -0
  177. clearskies/endpoints/health_check.py +183 -0
  178. clearskies/endpoints/list.py +574 -0
  179. clearskies/endpoints/restful_api.py +427 -0
  180. clearskies/endpoints/schema.py +189 -0
  181. clearskies/endpoints/simple_search.py +286 -0
  182. clearskies/endpoints/update.py +193 -0
  183. clearskies/environment.py +104 -0
  184. clearskies/exceptions/__init__.py +19 -0
  185. clearskies/exceptions/authentication.py +2 -0
  186. clearskies/exceptions/authorization.py +2 -0
  187. clearskies/exceptions/client_error.py +2 -0
  188. clearskies/exceptions/input_errors.py +4 -0
  189. clearskies/exceptions/missing_dependency.py +2 -0
  190. clearskies/exceptions/moved_permanently.py +3 -0
  191. clearskies/exceptions/moved_temporarily.py +3 -0
  192. clearskies/exceptions/not_found.py +2 -0
  193. clearskies/functional/__init__.py +7 -0
  194. clearskies/functional/routing.py +92 -0
  195. clearskies/functional/string.py +112 -0
  196. clearskies/functional/validations.py +76 -0
  197. clearskies/input_outputs/__init__.py +13 -0
  198. clearskies/input_outputs/cli.py +171 -0
  199. clearskies/input_outputs/exceptions/__init__.py +2 -0
  200. clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
  201. clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
  202. clearskies/input_outputs/headers.py +45 -0
  203. clearskies/input_outputs/input_output.py +138 -0
  204. clearskies/input_outputs/programmatic.py +69 -0
  205. clearskies/input_outputs/py.typed +0 -0
  206. clearskies/input_outputs/wsgi.py +77 -0
  207. clearskies/model.py +1922 -0
  208. clearskies/py.typed +0 -0
  209. clearskies/query/__init__.py +12 -0
  210. clearskies/query/condition.py +223 -0
  211. clearskies/query/join.py +136 -0
  212. clearskies/query/query.py +196 -0
  213. clearskies/query/sort.py +27 -0
  214. clearskies/schema.py +82 -0
  215. clearskies/secrets/__init__.py +6 -0
  216. clearskies/secrets/additional_configs/__init__.py +32 -0
  217. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
  218. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
  219. clearskies/secrets/akeyless.py +182 -0
  220. clearskies/secrets/exceptions/__init__.py +1 -0
  221. clearskies/secrets/exceptions/not_found.py +2 -0
  222. clearskies/secrets/secrets.py +38 -0
  223. clearskies/security_header.py +15 -0
  224. clearskies/security_headers/__init__.py +11 -0
  225. clearskies/security_headers/cache_control.py +67 -0
  226. clearskies/security_headers/cors.py +50 -0
  227. clearskies/security_headers/csp.py +94 -0
  228. clearskies/security_headers/hsts.py +22 -0
  229. clearskies/security_headers/x_content_type_options.py +0 -0
  230. clearskies/security_headers/x_frame_options.py +0 -0
  231. clearskies/test_base.py +8 -0
  232. clearskies/typing.py +11 -0
  233. clearskies/validator.py +37 -0
  234. clearskies/validators/__init__.py +33 -0
  235. clearskies/validators/after_column.py +62 -0
  236. clearskies/validators/before_column.py +13 -0
  237. clearskies/validators/in_the_future.py +32 -0
  238. clearskies/validators/in_the_future_at_least.py +11 -0
  239. clearskies/validators/in_the_future_at_most.py +10 -0
  240. clearskies/validators/in_the_past.py +32 -0
  241. clearskies/validators/in_the_past_at_least.py +10 -0
  242. clearskies/validators/in_the_past_at_most.py +10 -0
  243. clearskies/validators/maximum_length.py +26 -0
  244. clearskies/validators/maximum_value.py +29 -0
  245. clearskies/validators/minimum_length.py +26 -0
  246. clearskies/validators/minimum_value.py +29 -0
  247. clearskies/validators/required.py +34 -0
  248. clearskies/validators/timedelta.py +59 -0
  249. clearskies/validators/unique.py +30 -0
  250. clear_skies-2.0.5.dist-info/RECORD +0 -4
  251. {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/WHEEL +0 -0
  252. {clear_skies-2.0.5.dist-info → clear_skies-2.0.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,32 @@
1
+ from .mysql_connection_dynamic_producer import MySQLConnectionDynamicProducer
2
+ from .mysql_connection_dynamic_producer_via_ssh_cert_bastion import MySQLConnectionDynamicProducerViaSSHCertBastion
3
+
4
+
5
+ def mysql_connection_dynamic_producer(producer_name=None, database_host=None, database_name=None):
6
+ return MySQLConnectionDynamicProducer(
7
+ producer_name=producer_name,
8
+ database_host=database_host,
9
+ database_name=database_name,
10
+ )
11
+
12
+
13
+ def mysql_connection_dynamic_producer_via_ssh_cert_bastion(
14
+ producer_name=None,
15
+ bastion_host=None,
16
+ bastion_username=None,
17
+ public_key_file_path=None,
18
+ cert_issuer_name=None,
19
+ database_host=None,
20
+ database_name=None,
21
+ local_proxy_port=None,
22
+ ):
23
+ return MySQLConnectionDynamicProducerViaSSHCertBastion(
24
+ producer_name=producer_name,
25
+ bastion_host=bastion_host,
26
+ bastion_username=bastion_username,
27
+ cert_issuer_name=cert_issuer_name,
28
+ public_key_file_path=public_key_file_path,
29
+ database_host=database_host,
30
+ database_name=database_name,
31
+ local_proxy_port=local_proxy_port,
32
+ )
@@ -0,0 +1,61 @@
1
+ import clearskies.di
2
+
3
+
4
+ class MySQLConnectionDynamicProducer(clearskies.di.additional_config.AdditionalConfig):
5
+ _producer_name = None
6
+ _database_host = None
7
+ _database_name = None
8
+
9
+ def __init__(self, producer_name=None, database_host=None, database_name=None):
10
+ self._producer_name = producer_name
11
+ self._database_host = database_host
12
+ self._database_name = database_name
13
+
14
+ def provide_connection_details(self, environment, secrets):
15
+ if not secrets:
16
+ raise ValueError(
17
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
18
+ but AKeyless itself wasn't configured. \
19
+ Try setting the AKeyless auth method via clearskies.secrets.akeyless_[jwt|saml|aws_iam]_auth()"
20
+ )
21
+
22
+ producer_name = (
23
+ self._producer_name
24
+ if self._producer_name is not None
25
+ else environment.get("akeyless_mysql_dynamic_producer", silent=True)
26
+ )
27
+ if not producer_name:
28
+ raise ValueError(
29
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
30
+ but I wasn't told the path to the dynamic producer. \
31
+ This can be set in an environment variable named 'akeyless_mysql_dynamic_producer'\
32
+ or it can be set in the configuration via the producer_name kwarg."
33
+ )
34
+ database_name = (
35
+ self._database_name if self._database_name is not None else environment.get("db_database", silent=True)
36
+ )
37
+ if not database_name:
38
+ raise ValueError(
39
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
40
+ but I wasn't told the name of the database. \
41
+ This can be set in an environment variable named 'db_database' \
42
+ or it can be set in the configuration via the database_name kwarg."
43
+ )
44
+ database_host = (
45
+ self._database_host if self._database_host is not None else environment.get("db_host", silent=True)
46
+ )
47
+ if not database_host:
48
+ raise ValueError(
49
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
50
+ but I wasn't told the host name of the database. \
51
+ This can be set in an environment variable named 'db_host' \
52
+ or it can be set in the configuration via the database_host kwarg."
53
+ )
54
+ credentials = secrets.get_dynamic_secret(producer_name)
55
+
56
+ return {
57
+ "username": credentials["user"],
58
+ "password": credentials["password"],
59
+ "host": database_host,
60
+ "database": database_name,
61
+ }
@@ -0,0 +1,160 @@
1
+ import os
2
+ import socket
3
+ import subprocess
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import clearskies.di
8
+
9
+
10
+ class MySQLConnectionDynamicProducerViaSSHCertBastion(clearskies.di.additional_config.AdditionalConfig):
11
+ _config = None
12
+
13
+ def __init__(
14
+ self,
15
+ producer_name=None,
16
+ bastion_host=None,
17
+ bastion_username=None,
18
+ public_key_file_path=None,
19
+ local_proxy_port=None,
20
+ cert_issuer_name=None,
21
+ database_host=None,
22
+ database_name=None,
23
+ ):
24
+ # not using kwargs because I want the argument list to be explicit
25
+ self.config = {
26
+ "producer_name": producer_name,
27
+ "bastion_host": bastion_host,
28
+ "bastion_username": bastion_username,
29
+ "public_key_file_path": public_key_file_path,
30
+ "local_proxy_port": local_proxy_port,
31
+ "cert_issuer_name": cert_issuer_name,
32
+ "database_host": database_host,
33
+ "database_name": database_name,
34
+ }
35
+
36
+ def provide_connection_details(self, environment, secrets):
37
+ if not secrets:
38
+ raise ValueError(
39
+ "I was asked to connect to a database via an AKeyless dynamic producer but AKeyless itself wasn't configured. Try setting the AKeyless auth method via clearskies.secrets.akeyless_[jwt|saml|aws_iam]_auth()"
40
+ )
41
+
42
+ home = str(Path.home())
43
+ default_public_key_file_path = f"{home}/.ssh/id_rsa.pub"
44
+
45
+ producer_name = self._fetch_config(environment, "producer_name", "akeyless_mysql_dynamic_producer")
46
+ bastion_host = self._get_bastion_host(environment)
47
+ bastion_username = self._fetch_config(environment, "bastion_username", "akeyless_mysql_bastion_username")
48
+ public_key_file_path = self._fetch_config(
49
+ environment,
50
+ "public_key_file_path",
51
+ "akeyless_mysql_bastion_public_key_file_path",
52
+ default=default_public_key_file_path,
53
+ )
54
+ cert_issuer_name = self._fetch_config(
55
+ environment, "cert_issuer_name", "akeyless_mysql_bastion_cert_issuer_name"
56
+ )
57
+ local_proxy_port = self._fetch_config(
58
+ environment, "local_proxy_port", "akeyless_mysql_bastion_local_proxy_port", default=8888
59
+ )
60
+ database_host = self._fetch_config(environment, "database_host", "db_host")
61
+ database_name = self._fetch_config(environment, "database_name", "db_database")
62
+
63
+ # Create the SSH tunnel (yeah, it's obnoxious)
64
+ self._create_tunnel(
65
+ secrets,
66
+ bastion_host,
67
+ bastion_username,
68
+ local_proxy_port,
69
+ cert_issuer_name,
70
+ public_key_file_path,
71
+ database_host,
72
+ )
73
+
74
+ # and now we can fetch credentials
75
+ credentials = secrets.get_dynamic_secret(producer_name)
76
+
77
+ return {
78
+ "username": credentials["user"],
79
+ "password": credentials["password"],
80
+ "host": "127.0.0.1",
81
+ "database": database_name,
82
+ "port": local_proxy_port,
83
+ }
84
+
85
+ def _get_bastion_host(self, environment):
86
+ return self._fetch_config(environment, "bastion_host", "akeyless_mysql_bastion_host")
87
+
88
+ def _fetch_config(self, environment, config_key_name, environment_key_name, default=None):
89
+ if self.config[config_key_name]:
90
+ return self.config[config_key_name]
91
+ from_environment = environment.get(environment_key_name, silent=True)
92
+ if from_environment:
93
+ return from_environment
94
+ if default is not None:
95
+ return default
96
+ raise ValueError(
97
+ f"I was asked to connect to a database via an AKeyless dynamic producer through an SSH bastion"
98
+ "with certificate auth, but I wasn't given a required configuration value: '{config_key_name}'."
99
+ "This can be set in the call to "
100
+ "`clearskies.backends.akeyless.mysql_connection_dynamic_producer_via_ssh_cert_bastion()` by providing the "
101
+ "'{config_key_name}' argument, or by setting an environment variable named '{environment_key_name}'."
102
+ )
103
+
104
+ def _create_tunnel(
105
+ self,
106
+ secrets,
107
+ bastion_host,
108
+ bastion_username,
109
+ local_proxy_port,
110
+ cert_issuer_name,
111
+ public_key_file_path,
112
+ database_host,
113
+ ):
114
+ # first see if the socket is already open, since we don't close it.
115
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
116
+ result = sock.connect_ex(("127.0.0.1", local_proxy_port))
117
+ if result == 0:
118
+ sock.close()
119
+ return
120
+
121
+ if not os.path.isfile(public_key_file_path):
122
+ raise ValueError(
123
+ f"I was asked to connect to AKeyless SSH with the public key file in '{public_key_file_path}',"
124
+ "but this file does not exist"
125
+ )
126
+
127
+ ssh_certificate = secrets.get_ssh_certificate(cert_issuer_name, bastion_username, public_key_file_path)
128
+
129
+ # We need to write the certificate out to the standard location that SSH expects it so that SSH can find it.
130
+ # I haven't found a good library for doing this in Python, so I'm relying on the ssh command
131
+ home = str(Path.home())
132
+ with open(f"{home}/.ssh/id_rsa-cert.pub", "w") as fp:
133
+ fp.write(ssh_certificate)
134
+
135
+ # and now we can do this thing.
136
+ tunnel_command = [
137
+ "ssh",
138
+ "-o",
139
+ "ConnectTimeout=2",
140
+ "-N",
141
+ "-L",
142
+ f"{local_proxy_port}:{database_host}:3306",
143
+ "-p",
144
+ "22",
145
+ f"{bastion_username}@{bastion_host}",
146
+ ]
147
+ subprocess.Popen(tunnel_command)
148
+ connected = False
149
+ attempts = 0
150
+ while not connected and attempts < 6:
151
+ attempts += 1
152
+ time.sleep(0.5)
153
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
154
+ result = sock.connect_ex(("127.0.0.1", local_proxy_port))
155
+ if result == 0:
156
+ connected = True
157
+ if not connected:
158
+ raise ValueError(
159
+ "Failed to open SSH tunnel. The following command was used: \n" + " ".join(tunnel_command)
160
+ )
@@ -0,0 +1,182 @@
1
+ import datetime
2
+ from typing import Any
3
+
4
+ import clearskies.configs
5
+ from clearskies.di import InjectableProperties, inject
6
+
7
+
8
+ class Akeyless(clearskies.Configurable, clearskies.di.InjectableProperties):
9
+ requests = clearskies.di.inject.Requests()
10
+ environment = clearskies.di.inject.Environment()
11
+ akeyless = clearskies.di.inject.ByName("akeyless")
12
+
13
+ access_id = clearskies.configs.String(required=True, regexp=r"^p-[\d\w]+$")
14
+ access_type = clearskies.configs.Select(["aws_iam", "saml", "jwt"], required=True)
15
+ api_host = clearskies.configs.String(default="https://api.akeyless.io")
16
+ profile = clearskies.configs.String(regexp=r"^[\d\w\-]+$")
17
+
18
+ _token_refresh: datetime.datetime = None # type: ignore
19
+ _token: str = ""
20
+ _api: Any = None
21
+
22
+ def __init__(self, access_id: str, access_type: str, jwt_env_key: str = "", api_host: str = "", profile: str = ""):
23
+ self.access_id = access_id
24
+ self.access_type = access_type
25
+ self.jwt_env_key = jwt_env_key
26
+ self.api_host = api_host
27
+ self.profile = profile
28
+ if self.access_type == "jwt" and not self.jwt_env_key:
29
+ raise ValueError("When using the JWT access type for Akeyless you must provide jwt_env_key")
30
+
31
+ self.finalize_and_validate_configuration()
32
+
33
+ @property
34
+ def api(self) -> Any:
35
+ if self._api is None:
36
+ configuration = self.akeyless.Configuration(host=self.api_host)
37
+ self._api = self.akeyless.V2Api(self.akeyless.ApiClient(configuration))
38
+ return self._api
39
+
40
+ def create(self, path: str, value: Any) -> bool:
41
+ res = self.api.create_secret(self.akeyless.CreateSecret(name=path, value=str(value), token=self._get_token()))
42
+ return True
43
+
44
+ def get(self, path: str, silent_if_not_found: bool = False) -> str:
45
+ try:
46
+ res = self._api.get_secret_value(self.akeyless.GetSecretValue(names=[path], token=self._get_token()))
47
+ except Exception as e:
48
+ if e.status == 404: # type: ignore
49
+ if silent_if_not_found:
50
+ return ""
51
+ raise KeyError(f"Secret '{path}' not found")
52
+ raise e
53
+ return res[path]
54
+
55
+ def get_dynamic_secret(self, path: str, args: dict[str, Any] | None = None) -> Any:
56
+ kwargs = {
57
+ "name": path,
58
+ "token": self._get_token(),
59
+ }
60
+ if args:
61
+ kwargs["args"] = args # type: ignore
62
+
63
+ return self._api.get_dynamic_secret_value(self.akeyless.GetDynamicSecretValue(**kwargs))
64
+
65
+ def get_rotated_secret(self, path: str, args: dict[str, Any] | None = None) -> Any:
66
+ kwargs = {
67
+ "names": path,
68
+ "token": self._get_token(),
69
+ }
70
+ if args:
71
+ kwargs["args"] = args # type: ignore
72
+
73
+ res = self._api.get_rotated_secret_value(self.akeyless.GetRotatedSecretValue(**kwargs))
74
+ return res
75
+
76
+ def list_secrets(self, path: str) -> list[Any]:
77
+ res = self._api.list_items(self.akeyless.ListItems(path=path, token=self._get_token()))
78
+ if not res.items:
79
+ return []
80
+
81
+ return [item.item_name for item in res.items]
82
+
83
+ def update(self, path: str, value: Any) -> None:
84
+ res = self._api.update_secret_val(
85
+ self.akeyless.UpdateSecretVal(name=path, value=str(value), token=self._get_token())
86
+ )
87
+
88
+ def upsert(self, path: str, value: Any) -> None:
89
+ try:
90
+ self.update(path, value)
91
+ except Exception as e:
92
+ self.create(path, value)
93
+
94
+ def list_sub_folders(self, main_folder: str) -> list[str]:
95
+ """Return the list of secrets/sub folders in the given folder."""
96
+ items = self._api.list_items(self.akeyless.ListItems(path=main_folder, token=self._get_token()))
97
+
98
+ # akeyless will return the absolute path and end in a slash but we only want the folder name
99
+ main_folder_string_len = len(main_folder)
100
+ return [sub_folder[main_folder_string_len:-1] for sub_folder in items.folders]
101
+
102
+ def get_ssh_certificate(self, cert_issuer: str, cert_username: str, path_to_public_file: str) -> Any:
103
+ with open(path_to_public_file, "r") as fp:
104
+ public_key = fp.read()
105
+
106
+ res = self._api.get_ssh_certificate(
107
+ self.akeyless.GetSSHCertificate(
108
+ cert_username=cert_username,
109
+ cert_issuer_name=cert_issuer,
110
+ public_key_data=public_key,
111
+ token=self._get_token(),
112
+ )
113
+ )
114
+
115
+ return res.data
116
+
117
+ def _get_token(self) -> str:
118
+ # AKeyless tokens live for an hour
119
+ if self._token is not None and (self._token_refresh - datetime.datetime.now()).total_seconds() > 10:
120
+ return self._token
121
+
122
+ auth_method_name = f"auth_{self.access_type}"
123
+ if not hasattr(self, auth_method_name):
124
+ raise ValueError(f"Requested Akeyless authentication with unsupported auth method: '{self.access_type}'")
125
+
126
+ self._token_refresh = datetime.datetime.now() + datetime.timedelta(hours=0.5)
127
+ self._token = getattr(self, auth_method_name)()
128
+ return self._token
129
+
130
+ def auth_aws_iam(self):
131
+ from akeyless_cloud_id import CloudId # type: ignore
132
+
133
+ res = self._api.auth(
134
+ self.akeyless.Auth(access_id=self.access_id, access_type="aws_iam", cloud_id=CloudId().generate())
135
+ )
136
+ return res.token
137
+
138
+ def auth_saml(self):
139
+ import os
140
+ from pathlib import Path
141
+
142
+ os.system(f"akeyless list-items --profile {self.profile} --path /not/a/real/path > /dev/null 2>&1")
143
+ home = str(Path.home())
144
+ with open(f"{home}/.akeyless/.tmp_creds/{self.profile}-{self.access_id}", "r") as creds_file:
145
+ credentials = creds_file.read()
146
+
147
+ # and now we can turn that into a token
148
+ response = self.requests.post(
149
+ "https://rest.akeyless.io/",
150
+ data={
151
+ "cmd": "static-creds-auth",
152
+ "access-id": self.access_id,
153
+ "creds": credentials.strip(),
154
+ },
155
+ )
156
+ return response.json()["token"]
157
+
158
+ def auth_jwt(self):
159
+ if not self.jwt_env_key:
160
+ raise ValueError(
161
+ "To use AKeyless JWT Auth, "
162
+ "you must specify the name of the ENV key to load the JWT from when configuring AKeyless"
163
+ )
164
+ res = self._api.auth(
165
+ self.akeyless.Auth(access_id=self.access_id, access_type="jwt", jwt=self.environment.get(self.jwt_env_key))
166
+ )
167
+ return res.token
168
+
169
+
170
+ class AkeylessSaml(Akeyless):
171
+ def __init__(self, access_id: str, api_host: str = "", profile: str = ""):
172
+ return super().__init__(access_id, "saml", api_host=api_host, profile=profile)
173
+
174
+
175
+ class AkeylessJwt(Akeyless):
176
+ def __init__(self, access_id: str, jwt_env_key: str = "", api_host: str = "", profile: str = ""):
177
+ return super().__init__(access_id, "jwt", jwt_env_key=jwt_env_key, api_host=api_host, profile=profile)
178
+
179
+
180
+ class AkeylessAwsIam(Akeyless):
181
+ def __init__(self, access_id: str, api_host: str = ""):
182
+ return super().__init__(access_id, "aws_iam", api_host=api_host)
@@ -0,0 +1 @@
1
+ from .not_found import NotFound
@@ -0,0 +1,2 @@
1
+ class NotFound(Exception):
2
+ pass
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+
4
+ class Secrets:
5
+ def create(self, path: str, value: str) -> None:
6
+ raise NotImplementedError(
7
+ "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
8
+ )
9
+
10
+ def get(self, path: str, silent_if_not_found: bool = False) -> str:
11
+ raise NotImplementedError(
12
+ "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
13
+ )
14
+
15
+ def get_dynamic_secret(self, path: str, args: dict[str, Any] | None = None) -> Any:
16
+ raise NotImplementedError(
17
+ "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
18
+ )
19
+
20
+ def list_secrets(self, path: str) -> list[Any]:
21
+ raise NotImplementedError(
22
+ "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
23
+ )
24
+
25
+ def update(self, path: str, value: Any) -> None:
26
+ raise NotImplementedError(
27
+ "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
28
+ )
29
+
30
+ def upsert(self, path: str, value: Any) -> None:
31
+ raise NotImplementedError(
32
+ "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
33
+ )
34
+
35
+ def list_sub_folders(self, path: str) -> list[Any]:
36
+ raise NotImplementedError(
37
+ "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
38
+ )
@@ -0,0 +1,15 @@
1
+ from clearskies.configurable import Configurable
2
+
3
+
4
+ class SecurityHeader(Configurable):
5
+ """
6
+ Attach all the various security headers to endpoints.
7
+
8
+ The security header classes can be attached directly to both endpoints and endpoint groups and
9
+ are used to set all the various security headers.
10
+ """
11
+
12
+ is_cors = False
13
+
14
+ def set_headers_for_input_output(self, input_output):
15
+ raise NotImplementedError()
@@ -0,0 +1,11 @@
1
+ from clearskies.security_headers.cache_control import CacheControl
2
+ from clearskies.security_headers.cors import Cors
3
+ from clearskies.security_headers.csp import Csp
4
+ from clearskies.security_headers.hsts import Hsts
5
+
6
+ __all__ = [
7
+ "CacheControl",
8
+ "Cors",
9
+ "Csp",
10
+ "Hsts",
11
+ ]
@@ -0,0 +1,67 @@
1
+ import clearskies.configs
2
+ import clearskies.decorators
3
+ from clearskies.security_header import SecurityHeader
4
+
5
+
6
+ class CacheControl(SecurityHeader):
7
+ max_age = clearskies.configs.Integer()
8
+ s_maxage = clearskies.configs.Integer()
9
+ stale_while_revalidate = clearskies.configs.Integer()
10
+ stale_if_error = clearskies.configs.Integer()
11
+ immutable = clearskies.configs.Boolean(default=False)
12
+ must_understand = clearskies.configs.Boolean(default=False)
13
+ no_cache = clearskies.configs.Boolean(default=False)
14
+ no_store = clearskies.configs.Boolean(default=False)
15
+ no_transform = clearskies.configs.Boolean(default=False)
16
+ private = clearskies.configs.Boolean(default=False)
17
+ public = clearskies.configs.Boolean(default=False)
18
+
19
+ numbers: list[str] = [
20
+ "max_age",
21
+ "stale_if_error",
22
+ "stale_while_revalidate",
23
+ "s_maxage",
24
+ ]
25
+ bools: list[str] = [
26
+ "immutable",
27
+ "must_understand",
28
+ "no_cache",
29
+ "no_store",
30
+ "no_transform",
31
+ "private",
32
+ "public",
33
+ ]
34
+
35
+ @clearskies.decorators.parameters_to_properties
36
+ def __init__(
37
+ self,
38
+ max_age: int | None = None,
39
+ s_maxage: int | None = None,
40
+ stale_while_revalidate: int | None = None,
41
+ stale_if_error: int | None = None,
42
+ immutable: bool = False,
43
+ must_understand: bool = False,
44
+ no_cache: bool = False,
45
+ no_store: bool = False,
46
+ no_transform: bool = False,
47
+ private: bool = False,
48
+ public: bool = False,
49
+ ):
50
+ self.finalize_and_validate_configuration()
51
+
52
+ def set_headers_for_input_output(self, input_output):
53
+ parts = []
54
+ for variable_name in self.bools:
55
+ value = getattr(self, variable_name)
56
+ if not value:
57
+ continue
58
+ parts.append(variable_name.replace("_", "-"))
59
+ for variable_name in self.numbers:
60
+ value = getattr(self, variable_name)
61
+ if value is None:
62
+ continue
63
+ key_name = variable_name.replace("_", "-")
64
+ parts.append(f"{key_name}={value}")
65
+ if not parts:
66
+ return
67
+ input_output.response_headers.add("cache-control", ", ".join(parts))
@@ -0,0 +1,50 @@
1
+ import clearskies.configs
2
+ import clearskies.decorators
3
+ from clearskies.security_header import SecurityHeader
4
+
5
+
6
+ class Cors(SecurityHeader):
7
+ origin = clearskies.configs.String()
8
+ methods = clearskies.configs.StringList(default=[])
9
+ headers = clearskies.configs.StringList(default=[])
10
+ max_age = clearskies.configs.Integer(default=5)
11
+ credentials = clearskies.configs.Boolean(default=False)
12
+ expose_headers = clearskies.configs.StringList(default=[])
13
+ is_cors = True
14
+
15
+ @clearskies.decorators.parameters_to_properties
16
+ def __init__(
17
+ self,
18
+ credentials: bool = False,
19
+ expose_headers: list[str] = [],
20
+ headers: list[str] = [],
21
+ max_age: int = 5,
22
+ methods: list[str] = [],
23
+ origin: str = "",
24
+ ):
25
+ self.finalize_and_validate_configuration()
26
+
27
+ def set_headers(self, headers: list[str]):
28
+ self.headers = headers
29
+
30
+ def add_header(self, header: str):
31
+ self.headers = [*self.headers, header]
32
+
33
+ def set_methods(self, methods: list[str]):
34
+ self.methods = methods
35
+
36
+ def add_method(self, method: str):
37
+ self.methods = [*self.methods, method]
38
+
39
+ def set_headers_for_input_output(self, input_output):
40
+ for key in ["expose_headers", "methods", "headers"]:
41
+ value = getattr(self, key)
42
+ if not value:
43
+ continue
44
+ input_output.response_headers.add(f"access-control-allow-{key}".replace("_", "-"), ", ".join(value))
45
+ if self.credentials:
46
+ input_output.response_headers.add("access-control-allow-credentials", "true")
47
+ if self.max_age:
48
+ input_output.response_headers.add("access-control-max-age", str(self.max_age))
49
+ if self.origin:
50
+ input_output.response_headers.add("access-control-allow-origin", str(self.origin))