clear-skies-aws 2.0.12__tar.gz → 2.0.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.pre-commit-config.yaml +1 -2
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/CHANGELOG.md +11 -0
- clear_skies_aws-2.0.13/LATEST_CHANGELOG.md +9 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/PKG-INFO +2 -2
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/pyproject.toml +5 -7
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/di/inject/__init__.py +2 -1
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/secrets/__init__.py +4 -4
- clear_skies_aws-2.0.13/src/clearskies_aws/secrets/cache_storage/__init__.py +9 -0
- clear_skies_aws-2.0.13/src/clearskies_aws/secrets/cache_storage/parameter_store_cache.py +118 -0
- clear_skies_aws-2.0.13/src/clearskies_aws/secrets/parameter_store.py +188 -0
- clear_skies_aws-2.0.13/src/clearskies_aws/secrets/secrets.py +33 -0
- clear_skies_aws-2.0.13/src/clearskies_aws/secrets/secrets_manager.py +193 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/uv.lock +39 -23
- clear_skies_aws-2.0.12/LATEST_CHANGELOG.md +0 -16
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/additional_configs/__init__.py +0 -62
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/additional_configs/iam_db_auth.py +0 -39
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +0 -96
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -80
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +0 -162
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/akeyless_with_ssm_cache.py +0 -60
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/parameter_store.py +0 -52
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/secrets.py +0 -16
- clear_skies_aws-2.0.12/src/clearskies_aws/secrets/secrets_manager.py +0 -96
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.copier-answers.yml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.editorconfig +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.github/workflows/create-version.yaml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.github/workflows/docs.yaml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.github/workflows/run-tests.yml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.github/workflows/tests-matrix.yaml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.github/workflows/tests.yaml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.gitignore +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.python-version +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/.vscode/settings.json +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/LICENSE +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/README.md +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/cliff.toml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/ruff.toml +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/actions/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/actions/action_aws.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/actions/assume_role.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/actions/ses.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/actions/sns.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/actions/sqs.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/actions/step_function.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/backends/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/backends/backend.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/backends/dynamo_db_backend.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/backends/dynamo_db_condition_parser.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/backends/dynamo_db_parti_ql_backend.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/backends/sqs_backend.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/configs/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/cli_web_socket_mock.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/lambda_alb.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/lambda_api_gateway.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/lambda_api_gateway_web_socket.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/lambda_invoke.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/lambda_sns.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/lambda_sqs_standard.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/contexts/lambda_step_function.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/cursors/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/cursors/iam/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/cursors/iam/rds_mysql.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/cursors/port_forwarding/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/cursors/port_forwarding/ssm.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/di/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/di/aws_additional_config_auto_import.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/di/inject/boto3.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/di/inject/boto3_session.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/di/inject/parameter_store.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/endpoints/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/endpoints/secrets_manager_rotation.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/endpoints/simple_body_routing.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/cli_web_socket_mock.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_alb.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_api_gateway.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_input_output.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_invoke.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_sns.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_sqs_standard.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/input_outputs/lambda_step_function.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/mocks/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/mocks/actions/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/mocks/actions/ses.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/mocks/actions/sns.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/mocks/actions/sqs.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/mocks/actions/step_function.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/models/__init__.py +0 -0
- {clear_skies_aws-2.0.12 → clear_skies_aws-2.0.13}/src/clearskies_aws/models/web_socket_connection_model.py +0 -0
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.13] - 2026-01-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Add support for aws secrets
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Update dependencies in [#13](https://github.com/clearskies-py/aws/pull/13)
|
|
15
|
+
- Update to clearskies >=2.0.39
|
|
16
|
+
|
|
8
17
|
## [2.0.12] - 2026-01-27
|
|
9
18
|
|
|
10
19
|
### Added
|
|
@@ -12,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
12
21
|
- Add stepfucntions context with variables to ENV vars
|
|
13
22
|
|
|
14
23
|
### Changed
|
|
24
|
+
- Bump version to v2.0.12 by @github-actions[bot]
|
|
15
25
|
- Merge pull request #11 from clearskies-py/stepfunctions-variables by @cmancone in [#11](https://github.com/clearskies-py/aws/pull/11)
|
|
16
26
|
- Update docstrings of stepfunctions
|
|
17
27
|
- Move Environment to the input output of step functions
|
|
@@ -166,6 +176,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
166
176
|
* @cmancone made their first contribution
|
|
167
177
|
* @ made their first contribution
|
|
168
178
|
* @github-actions[bot] made their first contribution
|
|
179
|
+
[2.0.13]: https://github.com/clearskies-py/aws/compare/v2.0.12..v2.0.13
|
|
169
180
|
[2.0.12]: https://github.com/clearskies-py/aws/compare/v2.0.11..v2.0.12
|
|
170
181
|
[2.0.11]: https://github.com/clearskies-py/aws/compare/v2.0.10..v2.0.11
|
|
171
182
|
[2.0.10]: https://github.com/clearskies-py/aws/compare/v2.0.9..v2.0.10
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clear-skies-aws
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.13
|
|
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.
|
|
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
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
name = "clear-skies-aws"
|
|
4
4
|
description = "clearskies bindings for working in AWS"
|
|
5
5
|
license = "MIT"
|
|
6
|
-
version = "2.0.
|
|
6
|
+
version = "2.0.13"
|
|
7
7
|
readme = "./README.md"
|
|
8
8
|
authors = [{name = "tnijboer"}, {name = "Conor Mancone", email = "cmancone@gmail.com"}]
|
|
9
9
|
requires-python = '>=3.11,<4.0'
|
|
10
10
|
dependencies = [
|
|
11
11
|
"awslambdaric>=3.1.1",
|
|
12
12
|
'boto3 (>=1.26.148,<2.0.0)',
|
|
13
|
-
"clear-skies>=2.0.
|
|
13
|
+
"clear-skies>=2.0.39,<3.0.0",
|
|
14
14
|
"jinja2>=3.1.6",
|
|
15
15
|
'types-boto3[dynamodb,sns,sqs,ses,ssm,secretsmanager,stepfunctions] (>=1.38.13,<2.0.0)',
|
|
16
16
|
]
|
|
@@ -47,11 +47,6 @@ exclude = [
|
|
|
47
47
|
"tests/**"
|
|
48
48
|
]
|
|
49
49
|
|
|
50
|
-
[tool.black]
|
|
51
|
-
line-length = 120
|
|
52
|
-
skip-magic-trailing-comma = false
|
|
53
|
-
preview = false
|
|
54
|
-
|
|
55
50
|
[tool.mypy]
|
|
56
51
|
python_version = "3.11"
|
|
57
52
|
|
|
@@ -86,3 +81,6 @@ dev = [
|
|
|
86
81
|
"types-boto3[boto3,essential,secretsmanager,ses,ssm,stepfunctions]>=1.40.40",
|
|
87
82
|
"types-requests>=2.32.4",
|
|
88
83
|
]
|
|
84
|
+
doc = [
|
|
85
|
+
"clear-skies-doc-builder>=2.0.7",
|
|
86
|
+
]
|
|
@@ -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
|
-
|
|
2
|
-
|
|
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
|
-
"
|
|
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):
|
|
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)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
from clearskies.exceptions.not_found import NotFound
|
|
8
|
+
from types_boto3_ssm import SSMClient
|
|
9
|
+
|
|
10
|
+
from clearskies_aws.secrets import secrets
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ParameterStore(secrets.Secrets[SSMClient]):
|
|
14
|
+
"""
|
|
15
|
+
Backend for managing secrets using AWS Systems Manager Parameter Store.
|
|
16
|
+
|
|
17
|
+
This class provides integration with AWS SSM Parameter Store, allowing you to store,
|
|
18
|
+
retrieve, update, and delete secrets. All values are stored as SecureString by default
|
|
19
|
+
for security.
|
|
20
|
+
|
|
21
|
+
Paths are automatically sanitized to comply with SSM parameter naming requirements.
|
|
22
|
+
AWS SSM parameter paths only allow: a-z, A-Z, 0-9, -, _, ., /, @, and :
|
|
23
|
+
Any disallowed characters in the path are replaced with hyphens.
|
|
24
|
+
|
|
25
|
+
### Example Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from clearskies_aws.secrets import ParameterStore
|
|
29
|
+
|
|
30
|
+
secrets = ParameterStore()
|
|
31
|
+
|
|
32
|
+
# Create/update a secret (stored as SecureString)
|
|
33
|
+
secrets.update("/my-app/database-password", "super-secret")
|
|
34
|
+
|
|
35
|
+
# Get a secret
|
|
36
|
+
password = secrets.get("/my-app/database-password")
|
|
37
|
+
|
|
38
|
+
# Delete a secret
|
|
39
|
+
secrets.delete("/my-app/database-password")
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
ssm: SSMClient
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
"""Initialize the Parameter Store backend."""
|
|
47
|
+
super().__init__()
|
|
48
|
+
|
|
49
|
+
def _sanitize_path(self, path: str) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Sanitize a secret path for use as an SSM parameter name.
|
|
52
|
+
|
|
53
|
+
AWS SSM parameter paths only allow a-z, A-Z, 0-9, -, _, ., /, @, and :
|
|
54
|
+
Any disallowed characters are replaced with hyphens.
|
|
55
|
+
"""
|
|
56
|
+
return re.sub(r"[^a-zA-Z0-9\-_\./@:]", "-", path)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def boto3_client(self) -> SSMClient:
|
|
60
|
+
"""
|
|
61
|
+
Return the boto3 SSM client.
|
|
62
|
+
|
|
63
|
+
Creates a new client if one doesn't exist yet, using the AWS_REGION environment variable.
|
|
64
|
+
"""
|
|
65
|
+
if hasattr(self, "ssm"):
|
|
66
|
+
return self.ssm
|
|
67
|
+
self.ssm = self.boto3.client(
|
|
68
|
+
"ssm",
|
|
69
|
+
region_name=self.environment.get("AWS_REGION"),
|
|
70
|
+
)
|
|
71
|
+
return self.ssm
|
|
72
|
+
|
|
73
|
+
def create(self, path: str, value: str) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Create a new parameter in Parameter Store.
|
|
76
|
+
|
|
77
|
+
This is an alias for update() since Parameter Store uses upsert semantics.
|
|
78
|
+
"""
|
|
79
|
+
return self.update(path, value)
|
|
80
|
+
|
|
81
|
+
def get(self, path: str, silent_if_not_found: bool = False) -> str | None: # type: ignore[override]
|
|
82
|
+
"""
|
|
83
|
+
Retrieve a parameter value from Parameter Store.
|
|
84
|
+
|
|
85
|
+
Returns the decrypted parameter value for the given path. If silent_if_not_found
|
|
86
|
+
is True, returns None when the parameter is not found instead of raising NotFound.
|
|
87
|
+
"""
|
|
88
|
+
sanitized_path = self._sanitize_path(path)
|
|
89
|
+
try:
|
|
90
|
+
result = self.boto3_client.get_parameter(Name=sanitized_path, WithDecryption=True)
|
|
91
|
+
except ClientError as e:
|
|
92
|
+
error = e.response.get("Error", {})
|
|
93
|
+
if error.get("Code") == "ResourceNotFoundException":
|
|
94
|
+
if silent_if_not_found:
|
|
95
|
+
return None
|
|
96
|
+
raise NotFound(f"Could not find secret '{path}' in parameter store")
|
|
97
|
+
raise e
|
|
98
|
+
return result["Parameter"].get("Value", "")
|
|
99
|
+
|
|
100
|
+
def list_secrets(self, path: str) -> list[str]:
|
|
101
|
+
"""
|
|
102
|
+
List parameters at the given path.
|
|
103
|
+
|
|
104
|
+
Returns a list of parameter names at the specified path (non-recursive).
|
|
105
|
+
"""
|
|
106
|
+
sanitized_path = self._sanitize_path(path)
|
|
107
|
+
response = self.boto3_client.get_parameters_by_path(Path=sanitized_path, Recursive=False)
|
|
108
|
+
return [parameter["Name"] for parameter in response["Parameters"] if "Name" in parameter]
|
|
109
|
+
|
|
110
|
+
def update(self, path: str, value: str) -> bool: # type: ignore[override]
|
|
111
|
+
"""
|
|
112
|
+
Update or create a secret as a SecureString.
|
|
113
|
+
|
|
114
|
+
Creates the parameter if it doesn't exist, or updates it if it does.
|
|
115
|
+
The value is stored as an encrypted SecureString using the default KMS key.
|
|
116
|
+
"""
|
|
117
|
+
sanitized_path = self._sanitize_path(path)
|
|
118
|
+
self.boto3_client.put_parameter(
|
|
119
|
+
Name=sanitized_path,
|
|
120
|
+
Value=value,
|
|
121
|
+
Type="SecureString",
|
|
122
|
+
Overwrite=True,
|
|
123
|
+
)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def upsert(self, path: str, value: str) -> bool: # type: ignore[override]
|
|
127
|
+
"""
|
|
128
|
+
Create or update a secret.
|
|
129
|
+
|
|
130
|
+
This is an alias for update() since Parameter Store uses upsert semantics.
|
|
131
|
+
"""
|
|
132
|
+
return self.update(path, value)
|
|
133
|
+
|
|
134
|
+
def delete(self, path: str) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Delete a parameter from Parameter Store.
|
|
137
|
+
|
|
138
|
+
Returns True if the parameter was deleted, False if it didn't exist.
|
|
139
|
+
"""
|
|
140
|
+
sanitized_path = self._sanitize_path(path)
|
|
141
|
+
try:
|
|
142
|
+
self.boto3_client.delete_parameter(Name=sanitized_path)
|
|
143
|
+
return True
|
|
144
|
+
except ClientError as e:
|
|
145
|
+
error = e.response.get("Error", {})
|
|
146
|
+
if error.get("Code") == "ParameterNotFound":
|
|
147
|
+
return False
|
|
148
|
+
raise e
|
|
149
|
+
|
|
150
|
+
def delete_many(self, paths: list[str]) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Delete multiple parameters from Parameter Store.
|
|
153
|
+
|
|
154
|
+
Deletes up to 10 parameters at a time (SSM limit). For larger lists,
|
|
155
|
+
recursively calls itself to delete in batches.
|
|
156
|
+
"""
|
|
157
|
+
if not paths:
|
|
158
|
+
return True
|
|
159
|
+
sanitized_paths = [self._sanitize_path(p) for p in paths]
|
|
160
|
+
self.boto3_client.delete_parameters(Names=sanitized_paths[:10])
|
|
161
|
+
if len(sanitized_paths) > 10:
|
|
162
|
+
return self.delete_many(paths[10:])
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def list_by_path(self, path: str, recursive: bool = True) -> list[str]:
|
|
166
|
+
"""
|
|
167
|
+
List all parameter names under a given path.
|
|
168
|
+
|
|
169
|
+
Returns a list of parameter names (not values) under the specified path.
|
|
170
|
+
Uses pagination to handle large result sets.
|
|
171
|
+
"""
|
|
172
|
+
sanitized_path = self._sanitize_path(path)
|
|
173
|
+
names: list[str] = []
|
|
174
|
+
paginator = self.boto3_client.get_paginator("get_parameters_by_path")
|
|
175
|
+
for page in paginator.paginate(Path=sanitized_path, Recursive=recursive):
|
|
176
|
+
names.extend([param["Name"] for param in page.get("Parameters", [])])
|
|
177
|
+
return names
|
|
178
|
+
|
|
179
|
+
def list_sub_folders(
|
|
180
|
+
self,
|
|
181
|
+
path: str,
|
|
182
|
+
) -> list[Any]:
|
|
183
|
+
"""
|
|
184
|
+
List sub-folders at the given path.
|
|
185
|
+
|
|
186
|
+
This operation is not supported by Parameter Store.
|
|
187
|
+
"""
|
|
188
|
+
raise NotImplementedError("Parameter store doesn't support list_sub_folders.")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import profile
|
|
4
|
+
from typing import Generic, Protocol, TypeVar
|
|
5
|
+
|
|
6
|
+
from clearskies.di.inject import Di, Environment
|
|
7
|
+
from clearskies.secrets import Secrets as BaseSecrets
|
|
8
|
+
|
|
9
|
+
from clearskies_aws.di import inject
|
|
10
|
+
|
|
11
|
+
|
|
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]):
|
|
22
|
+
boto3 = inject.Boto3()
|
|
23
|
+
environment = Environment()
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__()
|
|
27
|
+
if not self.environment.get("AWS_REGION", True):
|
|
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")
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
from clearskies.exceptions.not_found import NotFound
|
|
7
|
+
from types_boto3_secretsmanager import SecretsManagerClient
|
|
8
|
+
from types_boto3_secretsmanager.type_defs import SecretListEntryTypeDef
|
|
9
|
+
|
|
10
|
+
from clearskies_aws.secrets import secrets
|
|
11
|
+
|
|
12
|
+
|
|
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
|
+
|
|
41
|
+
secrets_manager: SecretsManagerClient
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
"""Initialize the Secrets Manager backend."""
|
|
45
|
+
super().__init__()
|
|
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
|
|
61
|
+
|
|
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
|
+
"""
|
|
69
|
+
calling_parameters = {
|
|
70
|
+
"SecretId": secret_id,
|
|
71
|
+
"SecretString": value,
|
|
72
|
+
"KmsKeyId": kms_key_id,
|
|
73
|
+
}
|
|
74
|
+
calling_parameters = {key: value for (key, value) in calling_parameters.items() if value}
|
|
75
|
+
result = self.boto3_client.create_secret(**calling_parameters)
|
|
76
|
+
return bool(result.get("ARN"))
|
|
77
|
+
|
|
78
|
+
def get( # type: ignore[override]
|
|
79
|
+
self,
|
|
80
|
+
secret_id: str,
|
|
81
|
+
version_id: str | None = None,
|
|
82
|
+
version_stage: str | None = None,
|
|
83
|
+
silent_if_not_found: bool = False,
|
|
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
|
+
"""
|
|
92
|
+
calling_parameters = {"SecretId": secret_id}
|
|
93
|
+
|
|
94
|
+
# Only add optional parameters if they are not None
|
|
95
|
+
if version_id:
|
|
96
|
+
calling_parameters["VersionId"] = version_id
|
|
97
|
+
if version_stage:
|
|
98
|
+
calling_parameters["VersionStage"] = version_stage
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
result = self.boto3_client.get_secret_value(**calling_parameters)
|
|
102
|
+
except ClientError as e:
|
|
103
|
+
error = e.response.get("Error", {})
|
|
104
|
+
if error.get("Code") == "ResourceNotFoundException":
|
|
105
|
+
if silent_if_not_found:
|
|
106
|
+
return None
|
|
107
|
+
raise NotFound(
|
|
108
|
+
f"Could not find secret '{secret_id}' with version '{version_id}' and stage '{version_stage}'"
|
|
109
|
+
)
|
|
110
|
+
raise e
|
|
111
|
+
if result.get("SecretString"):
|
|
112
|
+
return result.get("SecretString")
|
|
113
|
+
return result.get("SecretBinary")
|
|
114
|
+
|
|
115
|
+
def list_secrets(self, path: str) -> list[SecretListEntryTypeDef]: # type: ignore[override]
|
|
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(
|
|
122
|
+
Filters=[
|
|
123
|
+
{
|
|
124
|
+
"Key": "name",
|
|
125
|
+
"Values": [path],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
return results["SecretList"]
|
|
130
|
+
|
|
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
|
+
"""
|
|
138
|
+
calling_parameters = {
|
|
139
|
+
"SecretId": secret_id,
|
|
140
|
+
"SecretString": value,
|
|
141
|
+
}
|
|
142
|
+
if kms_key_id:
|
|
143
|
+
# If no KMS key is provided, we should not include it in the parameters
|
|
144
|
+
calling_parameters["KmsKeyId"] = kms_key_id
|
|
145
|
+
|
|
146
|
+
result = self.boto3_client.update_secret(**calling_parameters)
|
|
147
|
+
return bool(result.get("ARN"))
|
|
148
|
+
|
|
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
|
+
"""
|
|
156
|
+
calling_parameters = {
|
|
157
|
+
"SecretId": secret_id,
|
|
158
|
+
"SecretString": value,
|
|
159
|
+
}
|
|
160
|
+
if kms_key_id:
|
|
161
|
+
# If no KMS key is provided, we should not include it in the parameters
|
|
162
|
+
calling_parameters["KmsKeyId"] = kms_key_id
|
|
163
|
+
|
|
164
|
+
result = self.boto3_client.put_secret_value(**calling_parameters)
|
|
165
|
+
return bool(result.get("ARN"))
|
|
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
|
+
|
|
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
|
+
"""
|
|
193
|
+
raise NotImplementedError("Secrets Manager doesn't support list_sub_folders.")
|