clear-skies-aws 2.0.12__py3-none-any.whl → 2.0.14__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clear-skies-aws
3
- Version: 2.0.12
3
+ Version: 2.0.14
4
4
  Summary: clearskies bindings for working in AWS
5
5
  Project-URL: Repository, https://github.com/clearskies-py/clearskies-aws
6
6
  Project-URL: Issues, https://github.com/clearskies-py/clearskies-aws/issues
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3
17
17
  Requires-Python: <4.0,>=3.11
18
18
  Requires-Dist: awslambdaric>=3.1.1
19
19
  Requires-Dist: boto3<2.0.0,>=1.26.148
20
- Requires-Dist: clear-skies<3.0.0,>=2.0.37
20
+ Requires-Dist: clear-skies<3.0.0,>=2.0.39
21
21
  Requires-Dist: jinja2>=3.1.6
22
22
  Requires-Dist: types-boto3[dynamodb,secretsmanager,ses,sns,sqs,ssm,stepfunctions]<2.0.0,>=1.38.13
23
23
  Provides-Extra: akeyless
@@ -29,7 +29,7 @@ clearskies_aws/cursors/port_forwarding/__init__.py,sha256=LBcFYeIIfmGhxf3Ezn1KCh
29
29
  clearskies_aws/cursors/port_forwarding/ssm.py,sha256=2tEznKflMG5a8g1AzHWghHkMp4BdBFhewB8WigFR5F4,4878
30
30
  clearskies_aws/di/__init__.py,sha256=pLHSIKxS1oELOgttRuwM0yXdJRxjZKXQ6tPxme2db0U,222
31
31
  clearskies_aws/di/aws_additional_config_auto_import.py,sha256=94h_YsPBcdwMhqn0VAAfId1jLL5vCsk76kUrr-6ET_U,1275
32
- clearskies_aws/di/inject/__init__.py,sha256=5_x5_BBQwC6J4k5YLdTm1DfIDM-95zXz1L5a1nMrlrY,186
32
+ clearskies_aws/di/inject/__init__.py,sha256=uuQlXuGgVH1rwMeQtIEQS9NiVjSHm4H4WkctWIakPS8,272
33
33
  clearskies_aws/di/inject/boto3.py,sha256=yUDiEpR2Si6pKcLrqMOlQEUU0pi6MS1tXNdoyC2mjwk,408
34
34
  clearskies_aws/di/inject/boto3_session.py,sha256=11UYHz5kgrrx5lawoYaOFBm-QIoa45YUCMAOn4gT8Jo,383
35
35
  clearskies_aws/di/inject/parameter_store.py,sha256=g0uAVwQEywLO9bCcYLbDKuyYnYgVyrfcYsOBJWYGads,475
@@ -54,17 +54,13 @@ clearskies_aws/mocks/actions/sqs.py,sha256=y0Vq7IMbjlfT5JMNHfbPsq9XVZhUF-G0kdXzQ
54
54
  clearskies_aws/mocks/actions/step_function.py,sha256=ENEVy8Ai3vPymbQre5aWa5z2McBjlnopfsLxdO7oEbc,937
55
55
  clearskies_aws/models/__init__.py,sha256=tAU5cPGRSzSClNVRCBxzwlBq6eZO8fftuI3bG1jEyVQ,87
56
56
  clearskies_aws/models/web_socket_connection_model.py,sha256=5M1qfQHKuWMYPUDkwT48QPo2ROey7koizvWLfapsfow,7492
57
- clearskies_aws/secrets/__init__.py,sha256=0mqYja2ETBHJh4b3jgRhhJV1uGdZc9S7cUvcV5QByPs,445
58
- clearskies_aws/secrets/akeyless_with_ssm_cache.py,sha256=32HUS1KQ5F6Fu70HDtojqDL7VZvP_YDgbWLTTmNJvPA,2073
59
- clearskies_aws/secrets/parameter_store.py,sha256=nMpkbUnxHGcCoMD5T-weCS13f1RZA7ZHl24O7qXaZLE,1882
60
- clearskies_aws/secrets/secrets.py,sha256=aDMPj-tuXdRhh8YKMnsJe9V_VLrD8Cru-xUKs8cyDIY,485
61
- clearskies_aws/secrets/secrets_manager.py,sha256=hK10lVmEFJltTv7h2AkYR3ySCVXx1JVy56jWU1hyYso,3708
62
- clearskies_aws/secrets/additional_configs/__init__.py,sha256=0NFOMVod8tte_K0Jq1Qf7_DDBvp6aEE4wF4hddQaW8w,1927
63
- clearskies_aws/secrets/additional_configs/iam_db_auth.py,sha256=PwyiLaacpRfhBKzQBdvGWHUYf5Ymth1sG2ly7Z6RoR0,1238
64
- clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py,sha256=ABY29X-YvrE6vvNo6kVdf4DqyRNq5cFR5SfK7MNkltE,3463
65
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py,sha256=mLaplwvJLSbGh6oXgdOKL9Mv-6hLv5OUYCfEwHbHvLE,3700
66
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py,sha256=2VHOwto4I9gBwrpd2HGpL-Wr0T2S-jFjUhe2Ib8hNJ8,6596
67
- clear_skies_aws-2.0.12.dist-info/METADATA,sha256=Peh_b5gfQkU62yNOdIGRsPWVpVXsyGS0YeQGKZgcWe8,9077
68
- clear_skies_aws-2.0.12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
69
- clear_skies_aws-2.0.12.dist-info/licenses/LICENSE,sha256=MkEX8JF8kZxdyBpTTcB0YTd-xZpWnHvbRlw-pQh8u58,1069
70
- clear_skies_aws-2.0.12.dist-info/RECORD,,
57
+ clearskies_aws/secrets/__init__.py,sha256=5QWfe6IyHdAyfOJVZJ52qM5hTkw1siMJ6q6YW95-Jl8,345
58
+ clearskies_aws/secrets/parameter_store.py,sha256=qMvjSjZ8VJ5piKm7mhLua8w9vHcm9SXcjF6_spbmF_I,7403
59
+ clearskies_aws/secrets/secrets.py,sha256=P30Yx0pkxjPnwCNw8ixoUu--4B7EEsg6gpOlazrk4Oc,956
60
+ clearskies_aws/secrets/secrets_manager.py,sha256=VhqKw4W65y3RRoFt9Ws5g7Q8nv2ACqkDYwxwgRpU-sk,7026
61
+ clearskies_aws/secrets/cache_storage/__init__.py,sha256=A6_rUn95NQjJu_VDDNQ1mDDNye18QYGGhXM66orGnb8,255
62
+ clearskies_aws/secrets/cache_storage/parameter_store_cache.py,sha256=KXGZ5ITni0HunoQhGLK4puvsTxhiwXccnS8RORjwSIw,4036
63
+ clear_skies_aws-2.0.14.dist-info/METADATA,sha256=6NEtQp_GRMx4EP4en6ovZbdGQC5uGdAZn-8gQwPdHUI,9077
64
+ clear_skies_aws-2.0.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
65
+ clear_skies_aws-2.0.14.dist-info/licenses/LICENSE,sha256=MkEX8JF8kZxdyBpTTcB0YTd-xZpWnHvbRlw-pQh8u58,1069
66
+ clear_skies_aws-2.0.14.dist-info/RECORD,,
@@ -2,5 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from clearskies_aws.di.inject.boto3 import Boto3
4
4
  from clearskies_aws.di.inject.boto3_session import Boto3Session
5
+ from clearskies_aws.di.inject.parameter_store import ParameterStore
5
6
 
6
- __all__ = ["Boto3", "Boto3Session"]
7
+ __all__ = ["Boto3", "Boto3Session", "ParameterStore"]
@@ -1,5 +1,6 @@
1
- from clearskies_aws.secrets import additional_configs
2
- from clearskies_aws.secrets.akeyless_with_ssm_cache import AkeylessWithSsmCache
1
+ import importlib
2
+
3
+ from clearskies_aws.secrets import cache_storage
3
4
  from clearskies_aws.secrets.parameter_store import ParameterStore
4
5
  from clearskies_aws.secrets.secrets import Secrets
5
6
  from clearskies_aws.secrets.secrets_manager import SecretsManager
@@ -8,6 +9,5 @@ __all__ = [
8
9
  "Secrets",
9
10
  "ParameterStore",
10
11
  "SecretsManager",
11
- "AkeylessWithSsmCache",
12
- "additional_configs",
12
+ "cache_storage",
13
13
  ]
@@ -0,0 +1,9 @@
1
+ """
2
+ AWS-specific secret cache implementations.
3
+
4
+ This module provides cache storage backends for secrets using AWS services.
5
+ """
6
+
7
+ from clearskies_aws.secrets.cache_storage.parameter_store_cache import ParameterStoreCache
8
+
9
+ __all__ = ["ParameterStoreCache"]
@@ -0,0 +1,118 @@
1
+ """
2
+ AWS Parameter Store implementation of SecretCache.
3
+
4
+ This module provides a cache storage backend using AWS Systems Manager Parameter Store.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from clearskies.configs import Boolean, String
10
+ from clearskies.decorators import parameters_to_properties
11
+ from clearskies.di.inject import ByClass
12
+ from clearskies.secrets.cache_storage import SecretCache
13
+
14
+ from clearskies_aws.di import inject
15
+ from clearskies_aws.secrets import parameter_store
16
+
17
+
18
+ class ParameterStoreCache(SecretCache):
19
+ """
20
+ Cache storage backend using AWS Systems Manager Parameter Store.
21
+
22
+ This class implements the SecretCache interface to store cached secrets in AWS
23
+ Parameter Store. Paths are automatically sanitized by the underlying ParameterStore
24
+ to comply with SSM parameter naming requirements.
25
+
26
+ ### Example Usage
27
+
28
+ ```python
29
+ from clearskies.secrets import Akeyless
30
+ from clearskies_aws.secrets.secrets_cache import ParameterStoreCache
31
+
32
+ cache = ParameterStoreCache(prefix="/cache/secrets")
33
+ akeyless = Akeyless(
34
+ access_id="p-xxx",
35
+ access_type="aws_iam",
36
+ cache_storage=cache,
37
+ )
38
+
39
+ # First call fetches from Akeyless and caches in Parameter Store
40
+ secret = akeyless.get("/path/to/secret")
41
+
42
+ # Subsequent calls return from Parameter Store cache
43
+ secret = akeyless.get("/path/to/secret")
44
+
45
+ # Force refresh bypasses cache
46
+ secret = akeyless.get("/path/to/secret", refresh=True)
47
+ ```
48
+ """
49
+
50
+ boto3 = inject.Boto3()
51
+
52
+ parameter_store = ByClass(parameter_store.ParameterStore)
53
+
54
+ prefix = String(default=None)
55
+ allow_cleanup = Boolean(default=False)
56
+
57
+ @parameters_to_properties
58
+ def __init__(self, prefix: str | None = None, allow_cleanup: bool = False) -> None:
59
+ """
60
+ Initialize the Parameter Store cache.
61
+
62
+ The prefix is prepended to all secret paths when storing in Parameter Store.
63
+ This helps organize cached secrets and avoid conflicts with other parameters.
64
+ """
65
+ super().__init__()
66
+
67
+ def _build_path(self, path: str) -> str:
68
+ """
69
+ Build the full parameter path by prepending the prefix.
70
+
71
+ The path is sanitized by the underlying ParameterStore.
72
+ """
73
+ return f"{self.prefix}/{path.lstrip('/')}"
74
+
75
+ def get(self, path: str) -> str | None:
76
+ """
77
+ Retrieve a cached secret value from Parameter Store.
78
+
79
+ Returns the cached secret value for the given path, or None if not found.
80
+ """
81
+ ssm_name = self._build_path(path)
82
+ return self.parameter_store.get(ssm_name, silent_if_not_found=True)
83
+
84
+ def set(self, path: str, value: str, ttl: int | None = None) -> None:
85
+ """
86
+ Store a secret value in Parameter Store.
87
+
88
+ Stores the secret value as a SecureString parameter. Note that Parameter Store
89
+ does not natively support TTL, so the ttl parameter is ignored. Consider using
90
+ a cleanup process or Lambda function to remove stale cached secrets.
91
+ """
92
+ ssm_name = self._build_path(path)
93
+ self.parameter_store.update(ssm_name, value)
94
+
95
+ def delete(self, path: str) -> None:
96
+ """
97
+ Remove a secret from the Parameter Store cache.
98
+
99
+ Deletes the parameter at the given path. Does nothing if the parameter
100
+ doesn't exist.
101
+ """
102
+ ssm_name = self._build_path(path)
103
+ self.parameter_store.delete(ssm_name)
104
+
105
+ def clear(self) -> None:
106
+ """
107
+ Remove all cached secrets from Parameter Store under the configured prefix.
108
+
109
+ This method deletes all parameters under the cache prefix. Use with caution
110
+ in production environments.
111
+ """
112
+ if not self.allow_cleanup:
113
+ raise RuntimeError(
114
+ "Clearing the Parameter Store cache is not allowed. Set allow_cleanup=True to enable this operation."
115
+ )
116
+ names = self.parameter_store.list_by_path(self.prefix, recursive=True)
117
+ if names:
118
+ self.parameter_store.delete_many(names)
@@ -1,5 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
4
+ from typing import Any
5
+
6
+ from botocore.config import Config
3
7
  from botocore.exceptions import ClientError
4
8
  from clearskies.exceptions.not_found import NotFound
5
9
  from types_boto3_ssm import SSMClient
@@ -7,19 +11,103 @@ from types_boto3_ssm import SSMClient
7
11
  from clearskies_aws.secrets import secrets
8
12
 
9
13
 
10
- class ParameterStore(secrets.Secrets):
14
+ class ParameterStore(secrets.Secrets[SSMClient]):
15
+ """
16
+ Backend for managing secrets using AWS Systems Manager Parameter Store.
17
+
18
+ This class provides integration with AWS SSM Parameter Store, allowing you to store,
19
+ retrieve, update, and delete secrets. All values are stored as SecureString by default
20
+ for security.
21
+
22
+ Paths are automatically sanitized to comply with SSM parameter naming requirements.
23
+ AWS SSM parameter paths only allow: a-z, A-Z, 0-9, -, _, ., /, @, and :
24
+ Any disallowed characters in the path are replaced with hyphens.
25
+
26
+ The client is configured with adaptive retry mode which automatically handles
27
+ throttling exceptions with exponential backoff (up to 10 retries).
28
+
29
+ ### Example Usage
30
+
31
+ ```python
32
+ from clearskies_aws.secrets import ParameterStore
33
+
34
+ secrets = ParameterStore()
35
+
36
+ # Create/update a secret (stored as SecureString)
37
+ secrets.update("/my-app/database-password", "super-secret")
38
+
39
+ # Get a secret
40
+ password = secrets.get("/my-app/database-password")
41
+
42
+ # Delete a secret
43
+ secrets.delete("/my-app/database-password")
44
+ ```
45
+ """
46
+
11
47
  ssm: SSMClient
12
48
 
13
49
  def __init__(self):
50
+ """Initialize the Parameter Store backend."""
14
51
  super().__init__()
15
- self.ssm = self.boto3.client("ssm", region_name=self.environment.get("AWS_REGION"))
52
+
53
+ def _sanitize_path(self, path: str) -> str:
54
+ """
55
+ Sanitize a secret path for use as an SSM parameter name.
56
+
57
+ AWS SSM parameter paths only allow a-z, A-Z, 0-9, -, _, ., /, @, and :
58
+ Any disallowed characters are replaced with hyphens.
59
+ """
60
+ return re.sub(r"[^a-zA-Z0-9\-_\./@:]", "-", path)
61
+
62
+ @property
63
+ def boto3_client(self) -> SSMClient:
64
+ """
65
+ Return the boto3 SSM client.
66
+
67
+ Creates a new client if one doesn't exist yet, using the AWS_REGION environment variable.
68
+ Configured with adaptive retry mode for better throttling handling.
69
+ """
70
+ if hasattr(self, "ssm"):
71
+ return self.ssm
72
+
73
+ # Configure adaptive retry mode with increased max attempts for throttling
74
+ # Adaptive mode automatically adjusts retry behavior based on error responses
75
+ # and includes exponential backoff with jitter
76
+ retry_config = Config(
77
+ retries={
78
+ "max_attempts": 10,
79
+ "mode": "adaptive",
80
+ }
81
+ )
82
+
83
+ self.ssm = self.boto3.client(
84
+ "ssm",
85
+ region_name=self.environment.get("AWS_REGION"),
86
+ config=retry_config,
87
+ )
88
+ return self.ssm
16
89
 
17
90
  def create(self, path: str, value: str) -> bool:
91
+ """
92
+ Create a new parameter in Parameter Store.
93
+
94
+ This is an alias for update() since Parameter Store uses upsert semantics.
95
+ """
18
96
  return self.update(path, value)
19
97
 
20
98
  def get(self, path: str, silent_if_not_found: bool = False) -> str | None: # type: ignore[override]
99
+ """
100
+ Retrieve a parameter value from Parameter Store.
101
+
102
+ Returns the decrypted parameter value for the given path. If silent_if_not_found
103
+ is True, returns None when the parameter is not found instead of raising NotFound.
104
+
105
+ Throttling is handled automatically by boto3's adaptive retry mode configured
106
+ on the client (up to 10 retries with exponential backoff and jitter).
107
+ """
108
+ sanitized_path = self._sanitize_path(path)
21
109
  try:
22
- result = self.ssm.get_parameter(Name=path, WithDecryption=True)
110
+ result = self.boto3_client.get_parameter(Name=sanitized_path, WithDecryption=True)
23
111
  except ClientError as e:
24
112
  error = e.response.get("Error", {})
25
113
  if error.get("Code") == "ResourceNotFoundException":
@@ -30,23 +118,91 @@ class ParameterStore(secrets.Secrets):
30
118
  return result["Parameter"].get("Value", "")
31
119
 
32
120
  def list_secrets(self, path: str) -> list[str]:
33
- response = self.ssm.get_parameters_by_path(Path=path, Recursive=False)
121
+ """
122
+ List parameters at the given path.
123
+
124
+ Returns a list of parameter names at the specified path (non-recursive).
125
+ """
126
+ sanitized_path = self._sanitize_path(path)
127
+ response = self.boto3_client.get_parameters_by_path(Path=sanitized_path, Recursive=False)
34
128
  return [parameter["Name"] for parameter in response["Parameters"] if "Name" in parameter]
35
129
 
36
130
  def update(self, path: str, value: str) -> bool: # type: ignore[override]
37
- response = self.ssm.put_parameter(
38
- Name=path,
131
+ """
132
+ Update or create a secret as a SecureString.
133
+
134
+ Creates the parameter if it doesn't exist, or updates it if it does.
135
+ The value is stored as an encrypted SecureString using the default KMS key.
136
+ """
137
+ sanitized_path = self._sanitize_path(path)
138
+ self.boto3_client.put_parameter(
139
+ Name=sanitized_path,
39
140
  Value=value,
40
- Type="String",
141
+ Type="SecureString",
41
142
  Overwrite=True,
42
143
  )
43
144
  return True
44
145
 
45
146
  def upsert(self, path: str, value: str) -> bool: # type: ignore[override]
147
+ """
148
+ Create or update a secret.
149
+
150
+ This is an alias for update() since Parameter Store uses upsert semantics.
151
+ """
46
152
  return self.update(path, value)
47
153
 
154
+ def delete(self, path: str) -> bool:
155
+ """
156
+ Delete a parameter from Parameter Store.
157
+
158
+ Returns True if the parameter was deleted, False if it didn't exist.
159
+ """
160
+ sanitized_path = self._sanitize_path(path)
161
+ try:
162
+ self.boto3_client.delete_parameter(Name=sanitized_path)
163
+ return True
164
+ except ClientError as e:
165
+ error = e.response.get("Error", {})
166
+ if error.get("Code") == "ParameterNotFound":
167
+ return False
168
+ raise e
169
+
170
+ def delete_many(self, paths: list[str]) -> bool:
171
+ """
172
+ Delete multiple parameters from Parameter Store.
173
+
174
+ Deletes up to 10 parameters at a time (SSM limit). For larger lists,
175
+ recursively calls itself to delete in batches.
176
+ """
177
+ if not paths:
178
+ return True
179
+ sanitized_paths = [self._sanitize_path(p) for p in paths]
180
+ self.boto3_client.delete_parameters(Names=sanitized_paths[:10])
181
+ if len(sanitized_paths) > 10:
182
+ return self.delete_many(paths[10:])
183
+ return True
184
+
185
+ def list_by_path(self, path: str, recursive: bool = True) -> list[str]:
186
+ """
187
+ List all parameter names under a given path.
188
+
189
+ Returns a list of parameter names (not values) under the specified path.
190
+ Uses pagination to handle large result sets.
191
+ """
192
+ sanitized_path = self._sanitize_path(path)
193
+ names: list[str] = []
194
+ paginator = self.boto3_client.get_paginator("get_parameters_by_path")
195
+ for page in paginator.paginate(Path=sanitized_path, Recursive=recursive):
196
+ names.extend([param["Name"] for param in page.get("Parameters", [])])
197
+ return names
198
+
48
199
  def list_sub_folders(
49
200
  self,
50
201
  path: str,
51
- ) -> list[str]: # type: ignore[override]
202
+ ) -> list[Any]:
203
+ """
204
+ List sub-folders at the given path.
205
+
206
+ This operation is not supported by Parameter Store.
207
+ """
52
208
  raise NotImplementedError("Parameter store doesn't support list_sub_folders.")
@@ -1,12 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import profile
4
+ from typing import Generic, Protocol, TypeVar
5
+
3
6
  from clearskies.di.inject import Di, Environment
4
7
  from clearskies.secrets import Secrets as BaseSecrets
5
8
 
6
9
  from clearskies_aws.di import inject
7
10
 
8
11
 
9
- class Secrets(BaseSecrets):
12
+ class Boto3Client(Protocol):
13
+ """Protocol for boto3 clients to enable type-safe generic return types."""
14
+
15
+ ...
16
+
17
+
18
+ ClientT = TypeVar("ClientT", bound=Boto3Client)
19
+
20
+
21
+ class Secrets(BaseSecrets, Generic[ClientT]):
10
22
  boto3 = inject.Boto3()
11
23
  environment = Environment()
12
24
 
@@ -14,3 +26,8 @@ class Secrets(BaseSecrets):
14
26
  super().__init__()
15
27
  if not self.environment.get("AWS_REGION", True):
16
28
  raise ValueError("To use secrets manager you must use set the 'AWS_REGION' environment variable")
29
+
30
+ @property
31
+ def boto3_client(self) -> ClientT:
32
+ """Return the boto3 client for the secrets manager implementation."""
33
+ raise NotImplementedError("You must implement the client property in subclasses")
@@ -10,21 +10,69 @@ from types_boto3_secretsmanager.type_defs import SecretListEntryTypeDef
10
10
  from clearskies_aws.secrets import secrets
11
11
 
12
12
 
13
- class SecretsManager(secrets.Secrets):
13
+ class SecretsManager(secrets.Secrets[SecretsManagerClient]):
14
+ """
15
+ Backend for managing secrets using AWS Secrets Manager.
16
+
17
+ This class provides integration with AWS Secrets Manager, allowing you to store,
18
+ retrieve, update, and delete secrets. It supports versioning and KMS encryption.
19
+
20
+ ### Example Usage
21
+
22
+ ```python
23
+ from clearskies_aws.secrets import SecretsManager
24
+
25
+ secrets = SecretsManager()
26
+
27
+ # Create a new secret
28
+ secrets.create("my-app/database-password", "super-secret-password")
29
+
30
+ # Get a secret
31
+ password = secrets.get("my-app/database-password")
32
+
33
+ # Update a secret
34
+ secrets.update("my-app/database-password", "new-password")
35
+
36
+ # Delete a secret
37
+ secrets.delete("my-app/database-password")
38
+ ```
39
+ """
40
+
14
41
  secrets_manager: SecretsManagerClient
15
42
 
16
43
  def __init__(self):
44
+ """Initialize the Secrets Manager backend."""
17
45
  super().__init__()
18
- self.secrets_manager = self.boto3.client("secretsmanager", region_name=self.environment.get("AWS_REGION"))
46
+
47
+ @property
48
+ def boto3_client(self) -> SecretsManagerClient:
49
+ """
50
+ Return the boto3 Secrets Manager client.
51
+
52
+ Creates a new client if one doesn't exist yet, using the AWS_REGION environment variable.
53
+ """
54
+ if hasattr(self, "secrets_manager"):
55
+ return self.secrets_manager
56
+ self.secrets_manager = self.boto3.client(
57
+ "secretsmanager",
58
+ region_name=self.environment.get("AWS_REGION"),
59
+ )
60
+ return self.secrets_manager
19
61
 
20
62
  def create(self, secret_id: str, value: Any, kms_key_id: str | None = None) -> bool:
63
+ """
64
+ Create a new secret in Secrets Manager.
65
+
66
+ Creates a new secret with the given ID and value. Optionally encrypts the secret
67
+ with a custom KMS key. Returns True if the secret was created successfully.
68
+ """
21
69
  calling_parameters = {
22
70
  "SecretId": secret_id,
23
71
  "SecretString": value,
24
72
  "KmsKeyId": kms_key_id,
25
73
  }
26
74
  calling_parameters = {key: value for (key, value) in calling_parameters.items() if value}
27
- result = self.secrets_manager.create_secret(**calling_parameters)
75
+ result = self.boto3_client.create_secret(**calling_parameters)
28
76
  return bool(result.get("ARN"))
29
77
 
30
78
  def get( # type: ignore[override]
@@ -34,6 +82,13 @@ class SecretsManager(secrets.Secrets):
34
82
  version_stage: str | None = None,
35
83
  silent_if_not_found: bool = False,
36
84
  ) -> str | bytes | None:
85
+ """
86
+ Retrieve a secret value from Secrets Manager.
87
+
88
+ Returns the secret value for the given ID. Optionally retrieves a specific version
89
+ by version_id or version_stage. If silent_if_not_found is True, returns None when
90
+ the secret is not found instead of raising NotFound.
91
+ """
37
92
  calling_parameters = {"SecretId": secret_id}
38
93
 
39
94
  # Only add optional parameters if they are not None
@@ -43,7 +98,7 @@ class SecretsManager(secrets.Secrets):
43
98
  calling_parameters["VersionStage"] = version_stage
44
99
 
45
100
  try:
46
- result = self.secrets_manager.get_secret_value(**calling_parameters)
101
+ result = self.boto3_client.get_secret_value(**calling_parameters)
47
102
  except ClientError as e:
48
103
  error = e.response.get("Error", {})
49
104
  if error.get("Code") == "ResourceNotFoundException":
@@ -58,7 +113,12 @@ class SecretsManager(secrets.Secrets):
58
113
  return result.get("SecretBinary")
59
114
 
60
115
  def list_secrets(self, path: str) -> list[SecretListEntryTypeDef]: # type: ignore[override]
61
- results = self.secrets_manager.list_secrets(
116
+ """
117
+ List secrets matching the given path filter.
118
+
119
+ Returns a list of secret metadata entries that match the path filter.
120
+ """
121
+ results = self.boto3_client.list_secrets(
62
122
  Filters=[
63
123
  {
64
124
  "Key": "name",
@@ -69,6 +129,12 @@ class SecretsManager(secrets.Secrets):
69
129
  return results["SecretList"]
70
130
 
71
131
  def update(self, secret_id: str, value: str, kms_key_id: str | None = None) -> bool: # type: ignore[override]
132
+ """
133
+ Update an existing secret's value.
134
+
135
+ Updates the secret with the given ID to the new value. Optionally re-encrypts
136
+ with a different KMS key. Returns True if the update was successful.
137
+ """
72
138
  calling_parameters = {
73
139
  "SecretId": secret_id,
74
140
  "SecretString": value,
@@ -77,10 +143,16 @@ class SecretsManager(secrets.Secrets):
77
143
  # If no KMS key is provided, we should not include it in the parameters
78
144
  calling_parameters["KmsKeyId"] = kms_key_id
79
145
 
80
- result = self.secrets_manager.update_secret(**calling_parameters)
146
+ result = self.boto3_client.update_secret(**calling_parameters)
81
147
  return bool(result.get("ARN"))
82
148
 
83
149
  def upsert(self, secret_id: str, value: str, kms_key_id: str | None = None) -> bool: # type: ignore[override]
150
+ """
151
+ Create or update a secret value.
152
+
153
+ Creates a new version of the secret with the given value. This is useful for
154
+ rotating secrets. Returns True if the operation was successful.
155
+ """
84
156
  calling_parameters = {
85
157
  "SecretId": secret_id,
86
158
  "SecretString": value,
@@ -89,8 +161,33 @@ class SecretsManager(secrets.Secrets):
89
161
  # If no KMS key is provided, we should not include it in the parameters
90
162
  calling_parameters["KmsKeyId"] = kms_key_id
91
163
 
92
- result = self.secrets_manager.put_secret_value(**calling_parameters)
164
+ result = self.boto3_client.put_secret_value(**calling_parameters)
93
165
  return bool(result.get("ARN"))
94
166
 
167
+ def delete(self, secret_id: str, force_delete: bool = False) -> bool:
168
+ """
169
+ Delete a secret from Secrets Manager.
170
+
171
+ If force_delete is True, the secret is deleted immediately without recovery window.
172
+ Otherwise, the secret is scheduled for deletion with a 7-day recovery window.
173
+ Returns True if the secret was deleted, False if it didn't exist.
174
+ """
175
+ try:
176
+ if force_delete:
177
+ self.boto3_client.delete_secret(SecretId=secret_id, ForceDeleteWithoutRecovery=True)
178
+ else:
179
+ self.boto3_client.delete_secret(SecretId=secret_id)
180
+ return True
181
+ except ClientError as e:
182
+ error = e.response.get("Error", {})
183
+ if error.get("Code") == "ResourceNotFoundException":
184
+ return False
185
+ raise e
186
+
95
187
  def list_sub_folders(self, path: str, value: str) -> list[str]: # type: ignore[override]
188
+ """
189
+ List sub-folders at the given path.
190
+
191
+ This operation is not supported by Secrets Manager.
192
+ """
96
193
  raise NotImplementedError("Secrets Manager doesn't support list_sub_folders.")
@@ -1,62 +0,0 @@
1
- from .iam_db_auth import IAMDBAuth
2
- from .iam_db_auth_with_ssm import IAMDBAuthWithSSM
3
- from .mysql_connection_dynamic_producer_via_ssh_cert_bastion import MySQLConnectionDynamicProducerViaSSHCertBastion
4
- from .mysql_connection_dynamic_producer_via_ssm_bastion import MySQLConnectionDynamicProducerViaSSMBastion
5
-
6
-
7
- def mysql_connection_dynamic_producer_via_ssh_cert_bastion(
8
- producer_name=None,
9
- bastion_host=None,
10
- bastion_name=None,
11
- bastion_region=None,
12
- bastion_username=None,
13
- public_key_file_path=None,
14
- cert_issuer_name=None,
15
- database_host=None,
16
- database_name=None,
17
- local_proxy_port=None,
18
- ):
19
- return MySQLConnectionDynamicProducerViaSSHCertBastion(
20
- producer_name=producer_name,
21
- bastion_host=bastion_host,
22
- bastion_name=bastion_name,
23
- bastion_region=bastion_region,
24
- bastion_username=bastion_username,
25
- cert_issuer_name=cert_issuer_name,
26
- public_key_file_path=public_key_file_path,
27
- database_host=database_host,
28
- database_name=database_name,
29
- local_proxy_port=local_proxy_port,
30
- )
31
-
32
-
33
- def mysql_connection_dynamic_producer_via_ssm_bastion(
34
- producer_name=None,
35
- bastion_instance_id=None,
36
- bastion_name=None,
37
- bastion_region=None,
38
- bastion_username=None,
39
- public_key_file_path=None,
40
- database_host=None,
41
- database_name=None,
42
- local_proxy_port=None,
43
- ):
44
- return MySQLConnectionDynamicProducerViaSSMBastion(
45
- producer_name=producer_name,
46
- bastion_instance_id=bastion_instance_id,
47
- bastion_name=bastion_name,
48
- bastion_region=bastion_region,
49
- bastion_username=bastion_username,
50
- public_key_file_path=public_key_file_path,
51
- database_host=database_host,
52
- database_name=database_name,
53
- local_proxy_port=local_proxy_port,
54
- )
55
-
56
-
57
- def iam_db_auth():
58
- return IAMDBAuth()
59
-
60
-
61
- def iam_db_auth_with_ssm():
62
- return IAMDBAuthWithSSM()
@@ -1,39 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
-
5
- import clearskies
6
-
7
-
8
- class IAMDBAuth(clearskies.di.AdditionalConfig):
9
- def provide_boto3(self):
10
- import boto3
11
-
12
- return boto3
13
-
14
- def provide_connection_details(self, environment, boto3):
15
- """
16
- Make configuration values and environment variables customizable.
17
-
18
- Allows both the values and the environment variable names to be set for flexible configuration.
19
-
20
- Returns:
21
- dict: Connection details for IAM DB authentication.
22
- """
23
- endpoint = environment.get("db_endpoint")
24
- username = environment.get("db_username")
25
- database = environment.get("db_database")
26
- region = environment.get("AWS_REGION")
27
- ssl_ca_bundle_name = environment.get("ssl_ca_bundle_filename")
28
- os.environ["LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN"] = "1"
29
-
30
- rds_api = boto3.Session().client("rds")
31
- rds_token = rds_api.generate_db_auth_token(DBHostname=endpoint, Port="3306", DBUsername=username, Region=region)
32
-
33
- return {
34
- "username": username,
35
- "password": rds_token,
36
- "host": endpoint,
37
- "database": database,
38
- "ssl_ca": ssl_ca_bundle_name,
39
- }
@@ -1,96 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
-
5
- import clearskies
6
-
7
-
8
- class IAMDBAuthWithSSM(clearskies.di.AdditionalConfig):
9
- def provide_subprocess(self):
10
- import subprocess
11
-
12
- return subprocess
13
-
14
- def provide_socket(self):
15
- import socket
16
-
17
- return socket
18
-
19
- def provide_connection_details(self, environment, subprocess, socket, boto3):
20
- local_port = self.open_tunnel(environment, subprocess, socket, boto3)
21
-
22
- return {
23
- "host": "127.0.0.1",
24
- "database": environment.get("db_database"),
25
- "username": environment.get("db_username"),
26
- "password": self.get_password(environment, boto3),
27
- "ssl_ca": "rds-cert-bundle.pem",
28
- "port": local_port,
29
- }
30
-
31
- def get_password(self, environment, boto3):
32
- endpoint = environment.get("db_endpoint")
33
- username = environment.get("db_username")
34
- region = environment.get("db_region")
35
-
36
- rds_api = boto3.Session().client("rds", region_name=region)
37
- return rds_api.generate_db_auth_token(DBHostname=endpoint, Port="3306", DBUsername=username, Region=region)
38
-
39
- def open_tunnel(self, environment, subprocess, socket, boto3):
40
- endpoint = environment.get("db_endpoint")
41
- region = environment.get("db_region")
42
- instance_name = environment.get("instance_name")
43
- local_proxy_port = int(environment.get("local_proxy_port", "9000"))
44
-
45
- ec2_api = boto3.client("ec2", region_name=region)
46
- running_instances = ec2_api.describe_instances(
47
- Filters=[
48
- {"Name": "tag:Name", "Values": [instance_name]},
49
- {"Name": "instance-state-name", "Values": ["running"]},
50
- ],
51
- )
52
- instance_ids = []
53
- for reservation in running_instances["Reservations"]:
54
- for instance in reservation["Instances"]:
55
- instance_ids.append(instance["InstanceId"])
56
-
57
- if len(instance_ids) == 0:
58
- raise ValueError("Failed to launch SSM tunnel! Cannot find bastion!")
59
-
60
- instance_id = instance_ids.pop()
61
- self._connect_to_bastion(local_proxy_port, instance_id, endpoint, subprocess, socket)
62
- return local_proxy_port
63
-
64
- def _connect_to_bastion(self, local_proxy_port, instance_id, endpoint, subprocess, socket):
65
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
66
- result = sock.connect_ex(("127.0.0.1", local_proxy_port))
67
- if result == 0:
68
- sock.close()
69
- return
70
-
71
- tunnel_command = [
72
- "aws",
73
- "--region",
74
- "us-east-1",
75
- "ssm",
76
- "start-session",
77
- "--target",
78
- "{}".format(instance_id),
79
- "--document-name",
80
- "AWS-StartPortForwardingSessionToRemoteHost",
81
- '--parameters={{"host":["{}"], "portNumber":["3306"],"localPortNumber":["{}"]}}'.format(
82
- endpoint, local_proxy_port
83
- ),
84
- ]
85
-
86
- subprocess.Popen(tunnel_command)
87
- connected = False
88
- attempts = 0
89
- while not connected and attempts < 6:
90
- attempts += 1
91
- time.sleep(0.5)
92
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
93
- result = sock.connect_ex(("127.0.0.1", local_proxy_port))
94
- if result == 0:
95
- return
96
- raise ValueError("Failed to launch SSM tunnel with command: " + " ".join(tunnel_command))
@@ -1,80 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import socket
5
- import subprocess
6
- import time
7
- from pathlib import Path
8
-
9
- from clearskies.secrets.additional_configs import MySQLConnectionDynamicProducerViaSSHCertBastion as Base
10
-
11
-
12
- class MySQLConnectionDynamicProducerViaSSHCertBastion(Base):
13
- _config = None
14
- _boto3 = None
15
-
16
- def __init__(
17
- self,
18
- producer_name=None,
19
- bastion_region=None,
20
- bastion_name=None,
21
- bastion_host=None,
22
- bastion_username=None,
23
- public_key_file_path=None,
24
- local_proxy_port=None,
25
- cert_issuer_name=None,
26
- database_host=None,
27
- database_name=None,
28
- ):
29
- # not using kwargs because I want the argument list to be explicit
30
- self.config = {
31
- "producer_name": producer_name,
32
- "bastion_host": bastion_host,
33
- "bastion_region": bastion_region,
34
- "bastion_name": bastion_name,
35
- "bastion_username": bastion_username,
36
- "public_key_file_path": public_key_file_path,
37
- "local_proxy_port": local_proxy_port,
38
- "cert_issuer_name": cert_issuer_name,
39
- "database_host": database_host,
40
- "database_name": database_name,
41
- }
42
-
43
- def provide_connection_details(self, environment, secrets, boto3):
44
- self._boto3 = boto3
45
- return super().provide_connection_details(environment, secrets)
46
-
47
- def _get_bastion_host(self, environment):
48
- bastion_host = self._fetch_config(environment, "bastion_host", "akeyless_mysql_bastion_host", default="")
49
- bastion_name = self._fetch_config(environment, "bastion_name", "akeyless_mysql_bastion_name", default="")
50
- if bastion_host:
51
- return bastion_host
52
- if bastion_name:
53
- bastion_region = self._fetch_config(environment, "bastion_region", "akeyless_mysql_bastion_region")
54
- return self._public_ip_from_name(bastion_name, bastion_region)
55
- raise ValueError(
56
- f"I was asked to connect to a database via an AKeyless dynamic producer through an SSH bastion with certificate auth, but I'm missing some configuration. I need either the bastion host or the name of the instance in AWS. These can be set in the call to `clearskies.backends.akeyless_aws.mysql_connection_dynamic_producer_via_ssh_cert_bastion()` by providing the 'bastion_host' or 'bastion_name' argument, or by setting an environment variable named 'akeyless_mysql_bastion_host' or 'akeyless_mysql_bastion_name'."
57
- )
58
-
59
- def _public_ip_from_name(self, bastion_name, bastion_region):
60
- ec2 = self._boto3.client("ec2", region_name=bastion_region)
61
- response = ec2.describe_instances(
62
- Filters=[
63
- {"Name": "tag:Name", "Values": [bastion_name]},
64
- {"Name": "instance-state-name", "Values": ["running"]},
65
- ],
66
- )
67
- if not response.get("Reservations"):
68
- raise ValueError(
69
- f"Could not find a running instance with the designated bastion name, '{bastion_name}' in region '{bastion_region}'"
70
- )
71
- if not response.get("Reservations")[0].get("Instances"):
72
- raise ValueError(
73
- f"Could not find a running instance with the designated bastion name, '{bastion_name}' in region '{bastion_region}'"
74
- )
75
- instance = response.get("Reservations")[0].get("Instances")[0]
76
- if not instance.get("PublicIpAddress"):
77
- raise ValueError(
78
- f"I found the bastion instance with a name of '{bastion_name}' in region '{bastion_region}', but it doesn't have a public IP address"
79
- )
80
- return instance.get("PublicIpAddress")
@@ -1,162 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import socket
5
- import subprocess
6
- import time
7
- from pathlib import Path
8
-
9
- from .mysql_connection_dynamic_producer_via_ssh_cert_bastion import (
10
- MySQLConnectionDynamicProducerViaSSHCertBastion as Base,
11
- )
12
-
13
-
14
- class MySQLConnectionDynamicProducerViaSSMBastion(Base):
15
- _config = None
16
- _boto3 = None
17
-
18
- def __init__(
19
- self,
20
- producer_name=None,
21
- bastion_region=None,
22
- bastion_name=None,
23
- bastion_username=None,
24
- bastion_instance_id=None,
25
- public_key_file_path=None,
26
- local_proxy_port=None,
27
- database_host=None,
28
- database_name=None,
29
- ):
30
- # not using kwargs because I want the argument list to be explicit
31
- self.config = {
32
- "producer_name": producer_name,
33
- "bastion_instance_id": bastion_instance_id,
34
- "bastion_region": bastion_region,
35
- "bastion_name": bastion_name,
36
- "bastion_username": bastion_username,
37
- "public_key_file_path": public_key_file_path,
38
- "local_proxy_port": local_proxy_port,
39
- "database_host": database_host,
40
- "database_name": database_name,
41
- }
42
-
43
- def provide_connection_details(self, environment, secrets, boto3):
44
- self._boto3 = boto3
45
- if not secrets:
46
- raise ValueError(
47
- "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()"
48
- )
49
-
50
- producer_name = self._fetch_config(environment, "producer_name", "akeyless_mysql_dynamic_producer")
51
- bastion_username = self._fetch_config(environment, "bastion_username", "mysql_bastion_username", default="ssm")
52
- bastion_instance_id = self._get_bastion_instance_id(environment)
53
- public_key_file_path = self._fetch_config(
54
- environment, "public_key_file_path", "mysql_bastion_public_key_file_path"
55
- )
56
- local_proxy_port = self._fetch_config(
57
- environment, "local_proxy_port", "akeyless_mysql_bastion_local_proxy_port", default=8888
58
- )
59
- database_host = self._fetch_config(environment, "database_host", "db_host")
60
- database_name = self._fetch_config(environment, "database_name", "db_database")
61
-
62
- # Create the SSH tunnel (yeah, it's obnoxious)
63
- self._create_tunnel(
64
- secrets,
65
- bastion_instance_id,
66
- bastion_username,
67
- bastion_region,
68
- public_key_file_path,
69
- local_proxy_port,
70
- database_host,
71
- )
72
-
73
- # and now we can fetch credentials
74
- credentials = secrets.get_dynamic_secret(producer_name)
75
-
76
- return {
77
- "username": credentials["user"],
78
- "password": credentials["password"],
79
- "host": "127.0.0.1",
80
- "database": database_name,
81
- "port": local_proxy_port,
82
- }
83
-
84
- def _get_bastion_instance_id(self, environment):
85
- bastion_instance_id = self._fetch_config(
86
- environment, "bastion_instance_id", "mysql_bastion_instance_id", default=""
87
- )
88
- bastion_name = self._fetch_config(environment, "bastion_name", "mysql_bastion_name", default="")
89
- if bastion_instance_id:
90
- return bastion_instance_id
91
- if bastion_name:
92
- bastion_region = self._fetch_config(environment, "bastion_region", "mysql_bastion_region")
93
- return self._instance_id_from_name(bastion_name, bastion_region)
94
- raise ValueError(
95
- f"I was asked to connect to a database via an AKeyless dynamic producer through an SSH bastion with certificate auth, but I'm missing some configuration. I need either the bastion host or the name of the instance in AWS. These can be set in the call to `clearskies.backends.akeyless_aws.mysql_connection_dynamic_producer_via_ssh_cert_bastion()` by providing the 'bastion_host' or 'bastion_name' argument, or by setting an environment variable named 'akeyless_mysql_bastion_host' or 'akeyless_mysql_bastion_name'."
96
- )
97
-
98
- def _instance_id_from_name(self, bastion_name, bastion_region):
99
- ec2 = self._boto3.client("ec2", region_name=bastion_region)
100
- response = ec2.describe_instances(
101
- Filters=[
102
- {"Name": "tag:Name", "Values": [bastion_name]},
103
- {"Name": "instance-state-name", "Values": ["running"]},
104
- ],
105
- )
106
- if not response.get("Reservations"):
107
- raise ValueError(
108
- f"Could not find a running instance with the designated bastion name, '{bastion_name}' in region '{bastion_region}'"
109
- )
110
- if not response.get("Reservations")[0].get("Instances"):
111
- raise ValueError(
112
- f"Could not find a running instance with the designated bastion name, '{bastion_name}' in region '{bastion_region}'"
113
- )
114
- return response.get("Reservations")[0].get("Instances")[0]["InstanceId"]
115
-
116
- def _create_tunnel(
117
- self,
118
- secrets,
119
- bastion_instance_id,
120
- bastion_username,
121
- bastion_region,
122
- public_key_file_path,
123
- local_proxy_port,
124
- database_host,
125
- ):
126
- # first see if the socket is already open, since we don't close it.
127
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
128
- result = sock.connect_ex(("127.0.0.1", local_proxy_port))
129
- if result == 0:
130
- sock.close()
131
- return
132
-
133
- # and now we can do this thing.
134
- tunnel_command = [
135
- "ssh",
136
- "-i",
137
- public_key_file_path,
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_instance_id}",
146
- ]
147
- my_env = os.environ.copy()
148
- my_env["AWS_DEFAULT_REGION"] = bastion_region
149
- subprocess.Popen(tunnel_command)
150
- connected = False
151
- attempts = 0
152
- while not connected and attempts < 6:
153
- attempts += 1
154
- time.sleep(0.5)
155
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
156
- result = sock.connect_ex(("127.0.0.1", local_proxy_port))
157
- if result == 0:
158
- connected = True
159
- if not connected:
160
- raise ValueError(
161
- "Failed to open SSH tunnel. The following command was used: \n" + " ".join(tunnel_command)
162
- )
@@ -1,60 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import Any
5
-
6
- from clearskies.secrets.akeyless import Akeyless
7
- from types_boto3_ssm import SSMClient
8
-
9
- from clearskies_aws.secrets import parameter_store
10
-
11
-
12
- class AkeylessWithSsmCache(parameter_store.ParameterStore, Akeyless):
13
- def get(self, path: str, refresh: bool = False) -> str | None: # type: ignore[override]
14
- # AWS SSM parameter paths only allow a-z, A-Z, 0-9, -, _, ., /, @, and :
15
- # Replace any disallowed characters with hyphens
16
- ssm_name = re.sub(r"[^a-zA-Z0-9\-_\./@:]", "-", path)
17
- # if we're not forcing a refresh, then see if it is in paramater store
18
- if not refresh:
19
- missing = False
20
- try:
21
- response = self.ssm.get_parameter(Name=ssm_name, WithDecryption=True)
22
- except self.ssm.exceptions.ParameterNotFound:
23
- missing = True
24
- if not missing:
25
- value = response["Parameter"].get("Value", "")
26
- if value:
27
- return value
28
-
29
- # otherwise get it out of Akeyless
30
- value = str(super().get(path))
31
-
32
- # and make sure and store the new value in parameter store
33
- if value:
34
- self.ssm.put_parameter(
35
- Name=ssm_name,
36
- Value=value,
37
- Type="SecureString",
38
- Overwrite=True,
39
- )
40
-
41
- return value
42
-
43
- def update(self, path: str, value: Any) -> bool: # type: ignore[override]
44
- res = self._api.update_secret_val(
45
- self.akeyless.UpdateSecretVal(name=path, value=str(value), token=self._get_token())
46
- )
47
- self.ssm.put_parameter(
48
- Name=re.sub(r"[^a-zA-Z0-9\-_\./@:]", "-", path),
49
- Value=value,
50
- Type="SecureString",
51
- Overwrite=True,
52
- )
53
- return True
54
-
55
- def upsert(self, path: str, value: Any) -> bool: # type: ignore[override]
56
- try:
57
- self.update(path, value)
58
- except Exception as e:
59
- self.create(path, value)
60
- return True