clear-skies 2.0.5__py3-none-any.whl → 2.0.6__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.
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/METADATA +1 -1
- clear_skies-2.0.6.dist-info/RECORD +251 -0
- clearskies/__init__.py +61 -0
- clearskies/action.py +7 -0
- clearskies/authentication/__init__.py +15 -0
- clearskies/authentication/authentication.py +46 -0
- clearskies/authentication/authorization.py +16 -0
- clearskies/authentication/authorization_pass_through.py +20 -0
- clearskies/authentication/jwks.py +163 -0
- clearskies/authentication/public.py +5 -0
- clearskies/authentication/secret_bearer.py +553 -0
- clearskies/autodoc/__init__.py +8 -0
- clearskies/autodoc/formats/__init__.py +5 -0
- clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
- clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
- clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
- clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
- clearskies/autodoc/formats/oai3_json/request.py +68 -0
- clearskies/autodoc/formats/oai3_json/response.py +28 -0
- clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
- clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
- clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
- clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
- clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
- clearskies/autodoc/formats/oai3_json/test.json +1985 -0
- clearskies/autodoc/py.typed +0 -0
- clearskies/autodoc/request/__init__.py +15 -0
- clearskies/autodoc/request/header.py +6 -0
- clearskies/autodoc/request/json_body.py +6 -0
- clearskies/autodoc/request/parameter.py +8 -0
- clearskies/autodoc/request/request.py +47 -0
- clearskies/autodoc/request/url_parameter.py +6 -0
- clearskies/autodoc/request/url_path.py +6 -0
- clearskies/autodoc/response/__init__.py +5 -0
- clearskies/autodoc/response/response.py +9 -0
- clearskies/autodoc/schema/__init__.py +31 -0
- clearskies/autodoc/schema/array.py +10 -0
- clearskies/autodoc/schema/base64.py +8 -0
- clearskies/autodoc/schema/boolean.py +5 -0
- clearskies/autodoc/schema/date.py +5 -0
- clearskies/autodoc/schema/datetime.py +5 -0
- clearskies/autodoc/schema/double.py +5 -0
- clearskies/autodoc/schema/enum.py +17 -0
- clearskies/autodoc/schema/integer.py +6 -0
- clearskies/autodoc/schema/long.py +5 -0
- clearskies/autodoc/schema/number.py +6 -0
- clearskies/autodoc/schema/object.py +13 -0
- clearskies/autodoc/schema/password.py +5 -0
- clearskies/autodoc/schema/schema.py +11 -0
- clearskies/autodoc/schema/string.py +5 -0
- clearskies/backends/__init__.py +65 -0
- clearskies/backends/api_backend.py +1178 -0
- clearskies/backends/backend.py +136 -0
- clearskies/backends/cursor_backend.py +335 -0
- clearskies/backends/memory_backend.py +797 -0
- clearskies/backends/secrets_backend.py +106 -0
- clearskies/column.py +1233 -0
- clearskies/columns/__init__.py +71 -0
- clearskies/columns/audit.py +206 -0
- clearskies/columns/belongs_to_id.py +483 -0
- clearskies/columns/belongs_to_model.py +132 -0
- clearskies/columns/belongs_to_self.py +105 -0
- clearskies/columns/boolean.py +113 -0
- clearskies/columns/category_tree.py +275 -0
- clearskies/columns/category_tree_ancestors.py +51 -0
- clearskies/columns/category_tree_children.py +127 -0
- clearskies/columns/category_tree_descendants.py +48 -0
- clearskies/columns/created.py +95 -0
- clearskies/columns/created_by_authorization_data.py +116 -0
- clearskies/columns/created_by_header.py +99 -0
- clearskies/columns/created_by_ip.py +92 -0
- clearskies/columns/created_by_routing_data.py +97 -0
- clearskies/columns/created_by_user_agent.py +92 -0
- clearskies/columns/date.py +234 -0
- clearskies/columns/datetime.py +282 -0
- clearskies/columns/email.py +76 -0
- clearskies/columns/float.py +153 -0
- clearskies/columns/has_many.py +505 -0
- clearskies/columns/has_many_self.py +56 -0
- clearskies/columns/has_one.py +14 -0
- clearskies/columns/integer.py +160 -0
- clearskies/columns/json.py +128 -0
- clearskies/columns/many_to_many_ids.py +337 -0
- clearskies/columns/many_to_many_ids_with_data.py +274 -0
- clearskies/columns/many_to_many_models.py +158 -0
- clearskies/columns/many_to_many_pivots.py +134 -0
- clearskies/columns/phone.py +159 -0
- clearskies/columns/select.py +92 -0
- clearskies/columns/string.py +102 -0
- clearskies/columns/timestamp.py +164 -0
- clearskies/columns/updated.py +110 -0
- clearskies/columns/uuid.py +86 -0
- clearskies/configs/README.md +105 -0
- clearskies/configs/__init__.py +162 -0
- clearskies/configs/actions.py +43 -0
- clearskies/configs/any.py +13 -0
- clearskies/configs/any_dict.py +22 -0
- clearskies/configs/any_dict_or_callable.py +23 -0
- clearskies/configs/authentication.py +23 -0
- clearskies/configs/authorization.py +23 -0
- clearskies/configs/boolean.py +16 -0
- clearskies/configs/boolean_or_callable.py +18 -0
- clearskies/configs/callable_config.py +18 -0
- clearskies/configs/columns.py +34 -0
- clearskies/configs/conditions.py +30 -0
- clearskies/configs/config.py +24 -0
- clearskies/configs/datetime.py +18 -0
- clearskies/configs/datetime_or_callable.py +19 -0
- clearskies/configs/endpoint.py +23 -0
- clearskies/configs/endpoint_list.py +29 -0
- clearskies/configs/float.py +16 -0
- clearskies/configs/float_or_callable.py +18 -0
- clearskies/configs/integer.py +16 -0
- clearskies/configs/integer_or_callable.py +18 -0
- clearskies/configs/joins.py +30 -0
- clearskies/configs/list_any_dict.py +30 -0
- clearskies/configs/list_any_dict_or_callable.py +31 -0
- clearskies/configs/model_class.py +35 -0
- clearskies/configs/model_column.py +65 -0
- clearskies/configs/model_columns.py +56 -0
- clearskies/configs/model_destination_name.py +25 -0
- clearskies/configs/model_to_id_column.py +43 -0
- clearskies/configs/readable_model_column.py +9 -0
- clearskies/configs/readable_model_columns.py +9 -0
- clearskies/configs/schema.py +23 -0
- clearskies/configs/searchable_model_columns.py +9 -0
- clearskies/configs/security_headers.py +39 -0
- clearskies/configs/select.py +26 -0
- clearskies/configs/select_list.py +47 -0
- clearskies/configs/string.py +29 -0
- clearskies/configs/string_dict.py +32 -0
- clearskies/configs/string_list.py +32 -0
- clearskies/configs/string_list_or_callable.py +35 -0
- clearskies/configs/string_or_callable.py +18 -0
- clearskies/configs/timedelta.py +18 -0
- clearskies/configs/timezone.py +18 -0
- clearskies/configs/url.py +23 -0
- clearskies/configs/validators.py +45 -0
- clearskies/configs/writeable_model_column.py +9 -0
- clearskies/configs/writeable_model_columns.py +9 -0
- clearskies/configurable.py +76 -0
- clearskies/contexts/__init__.py +11 -0
- clearskies/contexts/cli.py +117 -0
- clearskies/contexts/context.py +98 -0
- clearskies/contexts/wsgi.py +76 -0
- clearskies/contexts/wsgi_ref.py +82 -0
- clearskies/decorators.py +33 -0
- clearskies/di/__init__.py +14 -0
- clearskies/di/additional_config.py +130 -0
- clearskies/di/additional_config_auto_import.py +17 -0
- clearskies/di/di.py +973 -0
- clearskies/di/inject/__init__.py +23 -0
- clearskies/di/inject/by_class.py +21 -0
- clearskies/di/inject/by_name.py +18 -0
- clearskies/di/inject/di.py +13 -0
- clearskies/di/inject/environment.py +14 -0
- clearskies/di/inject/input_output.py +20 -0
- clearskies/di/inject/now.py +13 -0
- clearskies/di/inject/requests.py +13 -0
- clearskies/di/inject/secrets.py +14 -0
- clearskies/di/inject/utcnow.py +13 -0
- clearskies/di/inject/uuid.py +15 -0
- clearskies/di/injectable.py +29 -0
- clearskies/di/injectable_properties.py +131 -0
- clearskies/di/test_module/__init__.py +6 -0
- clearskies/di/test_module/another_module/__init__.py +2 -0
- clearskies/di/test_module/module_class.py +5 -0
- clearskies/end.py +183 -0
- clearskies/endpoint.py +1314 -0
- clearskies/endpoint_group.py +336 -0
- clearskies/endpoints/__init__.py +25 -0
- clearskies/endpoints/advanced_search.py +526 -0
- clearskies/endpoints/callable.py +388 -0
- clearskies/endpoints/create.py +205 -0
- clearskies/endpoints/delete.py +139 -0
- clearskies/endpoints/get.py +271 -0
- clearskies/endpoints/health_check.py +183 -0
- clearskies/endpoints/list.py +574 -0
- clearskies/endpoints/restful_api.py +427 -0
- clearskies/endpoints/schema.py +189 -0
- clearskies/endpoints/simple_search.py +286 -0
- clearskies/endpoints/update.py +193 -0
- clearskies/environment.py +104 -0
- clearskies/exceptions/__init__.py +19 -0
- clearskies/exceptions/authentication.py +2 -0
- clearskies/exceptions/authorization.py +2 -0
- clearskies/exceptions/client_error.py +2 -0
- clearskies/exceptions/input_errors.py +4 -0
- clearskies/exceptions/missing_dependency.py +2 -0
- clearskies/exceptions/moved_permanently.py +3 -0
- clearskies/exceptions/moved_temporarily.py +3 -0
- clearskies/exceptions/not_found.py +2 -0
- clearskies/functional/__init__.py +7 -0
- clearskies/functional/routing.py +92 -0
- clearskies/functional/string.py +112 -0
- clearskies/functional/validations.py +76 -0
- clearskies/input_outputs/__init__.py +13 -0
- clearskies/input_outputs/cli.py +171 -0
- clearskies/input_outputs/exceptions/__init__.py +2 -0
- clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
- clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
- clearskies/input_outputs/headers.py +45 -0
- clearskies/input_outputs/input_output.py +138 -0
- clearskies/input_outputs/programmatic.py +69 -0
- clearskies/input_outputs/py.typed +0 -0
- clearskies/input_outputs/wsgi.py +77 -0
- clearskies/model.py +1922 -0
- clearskies/py.typed +0 -0
- clearskies/query/__init__.py +12 -0
- clearskies/query/condition.py +223 -0
- clearskies/query/join.py +136 -0
- clearskies/query/query.py +196 -0
- clearskies/query/sort.py +27 -0
- clearskies/schema.py +82 -0
- clearskies/secrets/__init__.py +6 -0
- clearskies/secrets/additional_configs/__init__.py +32 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
- clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
- clearskies/secrets/akeyless.py +182 -0
- clearskies/secrets/exceptions/__init__.py +1 -0
- clearskies/secrets/exceptions/not_found.py +2 -0
- clearskies/secrets/secrets.py +38 -0
- clearskies/security_header.py +15 -0
- clearskies/security_headers/__init__.py +11 -0
- clearskies/security_headers/cache_control.py +67 -0
- clearskies/security_headers/cors.py +50 -0
- clearskies/security_headers/csp.py +94 -0
- clearskies/security_headers/hsts.py +22 -0
- clearskies/security_headers/x_content_type_options.py +0 -0
- clearskies/security_headers/x_frame_options.py +0 -0
- clearskies/test_base.py +8 -0
- clearskies/typing.py +11 -0
- clearskies/validator.py +37 -0
- clearskies/validators/__init__.py +33 -0
- clearskies/validators/after_column.py +62 -0
- clearskies/validators/before_column.py +13 -0
- clearskies/validators/in_the_future.py +32 -0
- clearskies/validators/in_the_future_at_least.py +11 -0
- clearskies/validators/in_the_future_at_most.py +10 -0
- clearskies/validators/in_the_past.py +32 -0
- clearskies/validators/in_the_past_at_least.py +10 -0
- clearskies/validators/in_the_past_at_most.py +10 -0
- clearskies/validators/maximum_length.py +26 -0
- clearskies/validators/maximum_value.py +29 -0
- clearskies/validators/minimum_length.py +26 -0
- clearskies/validators/minimum_value.py +29 -0
- clearskies/validators/required.py +34 -0
- clearskies/validators/timedelta.py +59 -0
- clearskies/validators/unique.py +30 -0
- clear_skies-2.0.5.dist-info/RECORD +0 -4
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.dist-info}/WHEEL +0 -0
- {clear_skies-2.0.5.dist-info → clear_skies-2.0.6.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
|
+
}
|
clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py
ADDED
|
@@ -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,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))
|