clear-skies-aws 2.0.1__py3-none-any.whl → 2.0.3__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.
- {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/METADATA +2 -2
- clear_skies_aws-2.0.3.dist-info/RECORD +63 -0
- {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/WHEEL +1 -1
- clearskies_aws/__init__.py +27 -0
- clearskies_aws/actions/__init__.py +15 -0
- clearskies_aws/actions/action_aws.py +135 -0
- clearskies_aws/actions/assume_role.py +115 -0
- clearskies_aws/actions/ses.py +203 -0
- clearskies_aws/actions/sns.py +61 -0
- clearskies_aws/actions/sqs.py +81 -0
- clearskies_aws/actions/step_function.py +73 -0
- clearskies_aws/backends/__init__.py +19 -0
- clearskies_aws/backends/backend.py +106 -0
- clearskies_aws/backends/dynamo_db_backend.py +609 -0
- clearskies_aws/backends/dynamo_db_condition_parser.py +325 -0
- clearskies_aws/backends/dynamo_db_parti_ql_backend.py +965 -0
- clearskies_aws/backends/sqs_backend.py +61 -0
- clearskies_aws/configs/__init__.py +0 -0
- clearskies_aws/contexts/__init__.py +23 -0
- clearskies_aws/contexts/cli_web_socket_mock.py +20 -0
- clearskies_aws/contexts/lambda_alb.py +81 -0
- clearskies_aws/contexts/lambda_api_gateway.py +81 -0
- clearskies_aws/contexts/lambda_api_gateway_web_socket.py +79 -0
- clearskies_aws/contexts/lambda_invoke.py +138 -0
- clearskies_aws/contexts/lambda_sns.py +124 -0
- clearskies_aws/contexts/lambda_sqs_standard.py +139 -0
- clearskies_aws/di/__init__.py +6 -0
- clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
- clearskies_aws/di/inject/__init__.py +6 -0
- clearskies_aws/di/inject/boto3.py +15 -0
- clearskies_aws/di/inject/boto3_session.py +13 -0
- clearskies_aws/di/inject/parameter_store.py +15 -0
- clearskies_aws/endpoints/__init__.py +1 -0
- clearskies_aws/endpoints/secrets_manager_rotation.py +194 -0
- clearskies_aws/endpoints/simple_body_routing.py +41 -0
- clearskies_aws/input_outputs/__init__.py +21 -0
- clearskies_aws/input_outputs/cli_web_socket_mock.py +20 -0
- clearskies_aws/input_outputs/lambda_alb.py +53 -0
- clearskies_aws/input_outputs/lambda_api_gateway.py +123 -0
- clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +73 -0
- clearskies_aws/input_outputs/lambda_input_output.py +89 -0
- clearskies_aws/input_outputs/lambda_invoke.py +88 -0
- clearskies_aws/input_outputs/lambda_sns.py +88 -0
- clearskies_aws/input_outputs/lambda_sqs_standard.py +86 -0
- clearskies_aws/mocks/__init__.py +1 -0
- clearskies_aws/mocks/actions/__init__.py +6 -0
- clearskies_aws/mocks/actions/ses.py +34 -0
- clearskies_aws/mocks/actions/sns.py +29 -0
- clearskies_aws/mocks/actions/sqs.py +29 -0
- clearskies_aws/mocks/actions/step_function.py +32 -0
- clearskies_aws/models/__init__.py +1 -0
- clearskies_aws/models/web_socket_connection_model.py +182 -0
- clearskies_aws/secrets/__init__.py +13 -0
- clearskies_aws/secrets/additional_configs/__init__.py +62 -0
- clearskies_aws/secrets/additional_configs/iam_db_auth.py +39 -0
- clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +96 -0
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +80 -0
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +162 -0
- clearskies_aws/secrets/akeyless_with_ssm_cache.py +60 -0
- clearskies_aws/secrets/parameter_store.py +52 -0
- clearskies_aws/secrets/secrets.py +16 -0
- clearskies_aws/secrets/secrets_manager.py +96 -0
- clear_skies_aws-2.0.1.dist-info/RECORD +0 -4
- {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from clearskies.authentication import Public
|
|
7
|
+
from clearskies.contexts.context import Context
|
|
8
|
+
|
|
9
|
+
from clearskies_aws.input_outputs import LambdaSqsStandard as LambdaSqsStandardInputOutput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LambdaSqsStandard(Context):
|
|
13
|
+
"""
|
|
14
|
+
Process messages from an SQS Standard Queue with Lambda.
|
|
15
|
+
|
|
16
|
+
Use this context when your application lives in a Lambda and is attached to an SQS standard
|
|
17
|
+
queue. Lambda always uses batch processing in this case, and will invoke your clearskies application
|
|
18
|
+
with a batch of messags. This clearskies context will then in turn invoke your application once
|
|
19
|
+
for every batched message. As a result, `request_data` will contain the contents of an individual message
|
|
20
|
+
from the queue, rather than the original group of batched events from Lambda. If any exception is thrown,
|
|
21
|
+
every other message in the queue will still be sent to your application, and clearskies will inform
|
|
22
|
+
AWS that the message and question failed to process.
|
|
23
|
+
|
|
24
|
+
### Usage
|
|
25
|
+
|
|
26
|
+
Here's a very simple example:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
import clearskies
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def some_function(request_data):
|
|
33
|
+
return print(request_data)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
lambda_sqs = clearskies_aws.contexts.LambdaSqsStandard(
|
|
37
|
+
clearskies.endpoints.Callable(
|
|
38
|
+
some_function,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def lambda_handler(event, context):
|
|
44
|
+
return lambda_sqs(event, context)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`lambda_handler` would then be attached to your lambda function, which is attached to some standard SQS.
|
|
48
|
+
|
|
49
|
+
Like the other lambda contexts which don't exist in an HTTP world, you can also attach a clearskies application
|
|
50
|
+
with routing and hard-code the path to invoke inside the lambda handler itself. This is handy if you have
|
|
51
|
+
a few related lambdas with similar configuration (since you only have to build a single application) or if
|
|
52
|
+
you have an application that already exists and you want to invoke some specific endpoint with an SQS:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
import clearskies
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def some_function(request_data):
|
|
59
|
+
return request_data
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def some_other_function(request_data):
|
|
63
|
+
return request_data
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def something_else(request_data):
|
|
67
|
+
return request_data
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
lambda_invoke = clearskies_aws.contexts.LambdaSqsStandard(
|
|
71
|
+
clearskies.endpoints.EndpointGroup(
|
|
72
|
+
[
|
|
73
|
+
clearskies.endpoints.Callable(
|
|
74
|
+
some_function,
|
|
75
|
+
url="some_function",
|
|
76
|
+
),
|
|
77
|
+
clearskies.endpoints.Callable(
|
|
78
|
+
some_other_function,
|
|
79
|
+
url="some_other_function",
|
|
80
|
+
),
|
|
81
|
+
clearskies.endpoints.Callable(
|
|
82
|
+
something_else,
|
|
83
|
+
url="something_else",
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def some_function_handler(event, context):
|
|
91
|
+
return lambda_invoke(event, context, url="some_function")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def some_other_function_handler(event, context):
|
|
95
|
+
return lambda_invoke(event, context, url="some_other_function")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def something_else_handler(event, context):
|
|
99
|
+
return lambda_invoke(event, context, url="something_else")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Context Specifics
|
|
103
|
+
|
|
104
|
+
When using this context, the following named parameters become available to inject into any callable
|
|
105
|
+
invoked by clearskies:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
| Name | Type | Description |
|
|
109
|
+
|:---------------------------:|:----------------:|--------------------------------------------------------|
|
|
110
|
+
| `event` | `dict[str, Any]` | The lambda `event` object |
|
|
111
|
+
| `context` | `dict[str, Any]` | The lambda `context` object |
|
|
112
|
+
| `message_id` | `str` | The AWS message id |
|
|
113
|
+
| `receipt_handle` | `str` | The receipt handle |
|
|
114
|
+
| `source_arn` | `str` | The ARN of the SQS the lambda is receiving events from |
|
|
115
|
+
| `sent_timestamp` | `str` | The timestamp when the message was sent |
|
|
116
|
+
| `approximate_receive_count` | `str` | The approximate receive count |
|
|
117
|
+
| `message_attributes` | `dict[str, Any]` | The message attributes |
|
|
118
|
+
| `record` | `dict[str, Any]` | The full record of the message being processed |
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __call__(
|
|
124
|
+
self, event: dict[str, Any], context: dict[str, Any], url: str = "", request_method: str = ""
|
|
125
|
+
) -> dict[str, Any]: # type: ignore[override]
|
|
126
|
+
item_failures = []
|
|
127
|
+
for record in event["Records"]:
|
|
128
|
+
try:
|
|
129
|
+
self.execute_application(
|
|
130
|
+
LambdaSqsStandardInputOutput(record, event, context, url=url, request_method=request_method)
|
|
131
|
+
)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
item_failures.append({"itemIdentifier": record["messageId"]})
|
|
134
|
+
|
|
135
|
+
if item_failures:
|
|
136
|
+
return {
|
|
137
|
+
"batchItemFailures": item_failures,
|
|
138
|
+
}
|
|
139
|
+
return {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from types import ModuleType
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import boto3 as boto3_module
|
|
6
|
+
from clearskies import Environment
|
|
7
|
+
from clearskies.di import AdditionalConfigAutoImport
|
|
8
|
+
from clearskies.di.additional_config import AdditionalConfig
|
|
9
|
+
|
|
10
|
+
from clearskies_aws.secrets import ParameterStore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AwsAdditionalConfigAutoImport(AdditionalConfigAutoImport):
|
|
14
|
+
"""
|
|
15
|
+
Provide a DI with AWS modules built-in.
|
|
16
|
+
|
|
17
|
+
This DI auto injects boto3, boto3 Session and the parameter store.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def provide_boto3(self) -> ModuleType:
|
|
21
|
+
import boto3
|
|
22
|
+
|
|
23
|
+
return boto3
|
|
24
|
+
|
|
25
|
+
def provide_parameter_store(self) -> ParameterStore:
|
|
26
|
+
# This is just here so that we can auto-inject the secrets into the environment without having
|
|
27
|
+
# to force the developer to define a secrets manager
|
|
28
|
+
return ParameterStore()
|
|
29
|
+
|
|
30
|
+
def provide_boto3_session(self, boto3: ModuleType, environment: Environment) -> boto3_module.session.Session:
|
|
31
|
+
if not environment.get("AWS_REGION", True):
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"To use AWS Session you must use set AWS_REGION in the .env file or an environment variable"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
session = boto3.session.Session(region_name=environment.get("AWS_REGION", True))
|
|
37
|
+
return session
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import ModuleType
|
|
4
|
+
|
|
5
|
+
from clearskies.di.injectable import Injectable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Boto3(Injectable):
|
|
9
|
+
def __init__(self, cache: bool = True):
|
|
10
|
+
self.cache = cache
|
|
11
|
+
|
|
12
|
+
def __get__(self, instance, parent) -> ModuleType:
|
|
13
|
+
if instance is None:
|
|
14
|
+
return self # type: ignore
|
|
15
|
+
return self._di.build_from_name("boto3", cache=self.cache)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from types import ModuleType
|
|
2
|
+
|
|
3
|
+
from clearskies.di.injectable import Injectable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Boto3Session(Injectable):
|
|
7
|
+
def __init__(self, cache: bool = True):
|
|
8
|
+
self.cache = cache
|
|
9
|
+
|
|
10
|
+
def __get__(self, instance, parent) -> ModuleType:
|
|
11
|
+
if instance is None:
|
|
12
|
+
return self # type: ignore
|
|
13
|
+
return self._di.build_from_name("boto3_session", cache=self.cache)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from clearskies.di.injectable import Injectable
|
|
2
|
+
|
|
3
|
+
from clearskies_aws.secrets.parameter_store import (
|
|
4
|
+
ParameterStore as ParameterStoreDependency,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ParameterStore(Injectable):
|
|
9
|
+
def __init__(self, cache: bool = True):
|
|
10
|
+
self.cache = cache
|
|
11
|
+
|
|
12
|
+
def __get__(self, instance, parent) -> ParameterStoreDependency:
|
|
13
|
+
if instance is None:
|
|
14
|
+
return self # type: ignore
|
|
15
|
+
return self._di.build_from_name("parameter_store", cache=self.cache)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .secrets_manager_rotation import SecretsManagerRotation
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import botocore
|
|
8
|
+
import clearskies
|
|
9
|
+
from clearskies import Endpoint
|
|
10
|
+
from clearskies.configs import Callable as CallableConfig
|
|
11
|
+
from clearskies.configs import Schema as SchemaConfig
|
|
12
|
+
from clearskies.configs import StringList
|
|
13
|
+
from clearskies.decorators import parameters_to_properties
|
|
14
|
+
from clearskies.di.inject import Di
|
|
15
|
+
from clearskies.exceptions import ClientError
|
|
16
|
+
from clearskies.input_outputs import InputOutput
|
|
17
|
+
|
|
18
|
+
from clearskies_aws.di import inject
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SecretsManagerRotation(Endpoint):
|
|
22
|
+
di = Di()
|
|
23
|
+
boto3 = inject.Boto3()
|
|
24
|
+
|
|
25
|
+
current = "AWSCURRENT"
|
|
26
|
+
pending = "AWSPENDING"
|
|
27
|
+
|
|
28
|
+
steps = StringList(default=["createSecret", "setSecret", "testSecret", "finishSecret"])
|
|
29
|
+
create_secret = CallableConfig(default=None)
|
|
30
|
+
set_secret = CallableConfig(default=None)
|
|
31
|
+
test_secret = CallableConfig(default=None)
|
|
32
|
+
finish_secret = CallableConfig(default=None)
|
|
33
|
+
schema = SchemaConfig(default=None)
|
|
34
|
+
|
|
35
|
+
@parameters_to_properties
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
steps: list[str] | None,
|
|
39
|
+
create_secret: Callable | None,
|
|
40
|
+
set_secret: Callable | None,
|
|
41
|
+
test_secret: Callable | None,
|
|
42
|
+
finish_secret: Callable | None,
|
|
43
|
+
schema: list[dict] | None,
|
|
44
|
+
):
|
|
45
|
+
super().__init__()
|
|
46
|
+
|
|
47
|
+
def configure(self):
|
|
48
|
+
self.finalize_and_validate_configuration()
|
|
49
|
+
class_name = self.__class__.__name__
|
|
50
|
+
if not self.create_secret:
|
|
51
|
+
raise KeyError(f"Missing required configuration 'createSecret' for handler {class_name}")
|
|
52
|
+
|
|
53
|
+
for config_name in self.steps:
|
|
54
|
+
config = getattr(self, config_name)
|
|
55
|
+
if config is None:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
def handle(self, input_output: InputOutput):
|
|
59
|
+
request_data = json.loads(input_output.get_body())
|
|
60
|
+
|
|
61
|
+
self.find_input_errors(request_data, input_output, self.schema)
|
|
62
|
+
|
|
63
|
+
arn = request_data.get("SecretId")
|
|
64
|
+
request_token = request_data.get("ClientRequestToken")
|
|
65
|
+
step = request_data.get("Step")
|
|
66
|
+
secretsmanager = self.boto3.client("secretsmanager")
|
|
67
|
+
metadata = secretsmanager.describe_secret(SecretId=arn)
|
|
68
|
+
|
|
69
|
+
self._validate_secret_and_request(step, arn, metadata, request_token)
|
|
70
|
+
|
|
71
|
+
current_secret_data = {}
|
|
72
|
+
pending_secret_data = {}
|
|
73
|
+
|
|
74
|
+
current_secret = secretsmanager.get_secret_value(SecretId=arn, VersionStage=self.current)
|
|
75
|
+
current_secret_data = json.loads(current_secret["SecretString"])
|
|
76
|
+
|
|
77
|
+
# validate the current secret
|
|
78
|
+
secret_errors = {
|
|
79
|
+
**self._extra_column_errors(current_secret_data),
|
|
80
|
+
**self._find_input_errors(current_secret_data),
|
|
81
|
+
}
|
|
82
|
+
if secret_errors:
|
|
83
|
+
raise ValueError(f"The current secret did not match the configured schema: {secret_errors}")
|
|
84
|
+
|
|
85
|
+
# check for a pending secret. Note that this is not always available. In the event that we are retrying a failed
|
|
86
|
+
# rotation it will already be set, in which case we need to skip the createSecret step.
|
|
87
|
+
try:
|
|
88
|
+
pending_secret = secretsmanager.get_secret_value(
|
|
89
|
+
SecretId=arn, VersionId=request_token, VersionStage=self.pending
|
|
90
|
+
)
|
|
91
|
+
pending_secret_data = json.loads(pending_secret["SecretString"])
|
|
92
|
+
except botocore.exceptions.ClientError as error:
|
|
93
|
+
if error.response["Error"]["Code"] == "ResourceNotFoundException":
|
|
94
|
+
pending_secret_data = None
|
|
95
|
+
else:
|
|
96
|
+
raise error
|
|
97
|
+
|
|
98
|
+
# we can't call the createSecret step if we already have a pending secret or this will generate an error from AWS.
|
|
99
|
+
if step == "createSecret" and pending_secret_data is not None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# call the appropriate step and pass along *everything*.
|
|
103
|
+
getattr(self, step)(
|
|
104
|
+
current_secret_data=current_secret_data,
|
|
105
|
+
pending_secret_data=pending_secret_data,
|
|
106
|
+
secretsmanager=secretsmanager,
|
|
107
|
+
metadata=metadata,
|
|
108
|
+
request_token=request_token,
|
|
109
|
+
arn=arn,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _validate_secret_and_request(self, step: str, arn: str, metadata: dict[str, Any], request_token: str):
|
|
113
|
+
"""Perform basic checks suggested by AWS of both the request and the secret to ensure validity."""
|
|
114
|
+
if step not in self.steps:
|
|
115
|
+
raise ClientError(f"Invalid step: {step}")
|
|
116
|
+
|
|
117
|
+
if not metadata.get("RotationEnabled"):
|
|
118
|
+
raise ValueError("Secret %s is not enabled for rotation" % arn)
|
|
119
|
+
|
|
120
|
+
versions = metadata["VersionIdsToStages"]
|
|
121
|
+
prefix = f"Rotation config error for version '{request_token}' of secret '{arn}': "
|
|
122
|
+
if request_token not in versions:
|
|
123
|
+
raise ValueError(f"{prefix} we don't have a stage for rotation")
|
|
124
|
+
if self.current in versions[request_token]:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"{prefix} it's already the current version, which shouldn't happen. I'm quitting with prejudice."
|
|
127
|
+
)
|
|
128
|
+
elif self.pending not in versions[request_token]:
|
|
129
|
+
raise ValueError(f"{prefix} it hasn't been set to pending yet, which makes no sense!")
|
|
130
|
+
|
|
131
|
+
def createSecret(self, **kwargs):
|
|
132
|
+
new_secret_data = self.di.call_function(self.create_secret, **kwargs)
|
|
133
|
+
if new_secret_data is None:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"I called the configured createSecret function but it didn't return anything. It has to return the new secret data."
|
|
136
|
+
)
|
|
137
|
+
if not isinstance(new_secret_data, dict):
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"I called the configured createSecret function but it didn't return a dictionary. The createSecret function must return a dictionary."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
secret_errors = {
|
|
143
|
+
**self._extra_column_errors(new_secret_data),
|
|
144
|
+
**self.find_input_errors(new_secret_data),
|
|
145
|
+
}
|
|
146
|
+
if secret_errors:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"The secret data returned by the call to createSecret did not match the configured schema: {secret_errors}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# if we get this far we can store the new data
|
|
152
|
+
secretsmanager = kwargs["secretsmanager"]
|
|
153
|
+
request_token = kwargs["request_token"]
|
|
154
|
+
arn = kwargs["arn"]
|
|
155
|
+
secretsmanager.put_secret_value(
|
|
156
|
+
SecretId=arn,
|
|
157
|
+
SecretString=json.dumps(new_secret_data),
|
|
158
|
+
ClientRequestToken=request_token,
|
|
159
|
+
VersionStages=[self.pending],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def setSecret(self, **kwargs):
|
|
163
|
+
if not self._configuration.get("setSecret"):
|
|
164
|
+
return
|
|
165
|
+
self._di.call_function(self._configuration["setSecret"], **kwargs)
|
|
166
|
+
|
|
167
|
+
def testSecret(self, **kwargs):
|
|
168
|
+
if not self._configuration.get("testSecret"):
|
|
169
|
+
return
|
|
170
|
+
self._di.call_function(self._configuration["testSecret"], **kwargs)
|
|
171
|
+
|
|
172
|
+
def finishSecret(self, **kwargs):
|
|
173
|
+
if self._configuration.get("finishSecret"):
|
|
174
|
+
self._di.call_function(self._configuration["finishSecret"], **kwargs)
|
|
175
|
+
|
|
176
|
+
secretsmanager = kwargs["secretsmanager"]
|
|
177
|
+
request_token = kwargs["request_token"]
|
|
178
|
+
arn = kwargs["arn"]
|
|
179
|
+
metadata = kwargs["metadata"]
|
|
180
|
+
current_version = None
|
|
181
|
+
for version in metadata["VersionIdsToStages"]:
|
|
182
|
+
if self.current not in metadata["VersionIdsToStages"][version]:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
if version == request_token:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
current_version = version
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
# finish the rotation by taking the new version and making it current.
|
|
192
|
+
secretsmanager.update_secret_version_stage(
|
|
193
|
+
SecretId=arn, VersionStage=self.current, MoveToVersionId=request_token, RemoveFromVersionId=current_version
|
|
194
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from clearskies import Endpoint
|
|
7
|
+
from clearskies.configs import AnyDict, String
|
|
8
|
+
from clearskies.di.inject import Di
|
|
9
|
+
from clearskies.input_outputs import InputOutput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SimpleBodyRouting(Endpoint):
|
|
13
|
+
di = Di()
|
|
14
|
+
|
|
15
|
+
route_key = String(default="route")
|
|
16
|
+
routes = AnyDict(default={})
|
|
17
|
+
|
|
18
|
+
def __init__(self, routes: dict[str, Any], route_key: str = "route"):
|
|
19
|
+
self.routes = routes
|
|
20
|
+
self.route_key = route_key
|
|
21
|
+
|
|
22
|
+
def handle(self, input_output: InputOutput) -> Any:
|
|
23
|
+
body = json.loads(input_output.get_body()) if input_output.has_body() else {}
|
|
24
|
+
|
|
25
|
+
if not body or not body.get(self.route_key):
|
|
26
|
+
return self.error(input_output, "Not Found", 404)
|
|
27
|
+
|
|
28
|
+
route = body[self.route_key]
|
|
29
|
+
if route not in self.routes:
|
|
30
|
+
return self.error(input_output, "Not Found", 404)
|
|
31
|
+
return input_output.respond(
|
|
32
|
+
self.di.call_function(
|
|
33
|
+
self.routes[route],
|
|
34
|
+
request_data=body,
|
|
35
|
+
**input_output.context_specifics(),
|
|
36
|
+
),
|
|
37
|
+
200,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def documentation(self):
|
|
41
|
+
return []
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from clearskies_aws.input_outputs.cli_web_socket_mock import CliWebSocketMock
|
|
4
|
+
from clearskies_aws.input_outputs.lambda_alb import LambdaAlb
|
|
5
|
+
from clearskies_aws.input_outputs.lambda_api_gateway import LambdaApiGateway
|
|
6
|
+
from clearskies_aws.input_outputs.lambda_api_gateway_web_socket import (
|
|
7
|
+
LambdaApiGatewayWebSocket,
|
|
8
|
+
)
|
|
9
|
+
from clearskies_aws.input_outputs.lambda_invoke import LambdaInvoke
|
|
10
|
+
from clearskies_aws.input_outputs.lambda_sns import LambdaSns
|
|
11
|
+
from clearskies_aws.input_outputs.lambda_sqs_standard import LambdaSqsStandard
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CliWebSocketMock",
|
|
15
|
+
"LambdaApiGateway",
|
|
16
|
+
"LambdaApiGatewayWebSocket",
|
|
17
|
+
"LambdaAlb",
|
|
18
|
+
"LambdaInvoke",
|
|
19
|
+
"LambdaSns",
|
|
20
|
+
"LambdaSqsStandard",
|
|
21
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import clearskies
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CliWebSocketMock(clearskies.input_outputs.Cli):
|
|
9
|
+
def context_specifics(self):
|
|
10
|
+
connection_id = json.loads(self.get_body()).get("connection_id")
|
|
11
|
+
if not connection_id:
|
|
12
|
+
raise KeyError("When using the CliWebsocketMock you must provide connection_id in the request body")
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
"event": {},
|
|
16
|
+
"context": {},
|
|
17
|
+
"connection_id": connection_id,
|
|
18
|
+
"domain": "",
|
|
19
|
+
"stage": "",
|
|
20
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from clearskies.configs import String
|
|
6
|
+
from clearskies.input_outputs import Headers
|
|
7
|
+
|
|
8
|
+
from clearskies_aws.input_outputs import lambda_input_output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LambdaAlb(lambda_input_output.LambdaInputOutput):
|
|
12
|
+
"""Application Load Balancer specific Lambda input/output handler."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, event: dict[str, Any], context: dict[str, Any]):
|
|
15
|
+
# Call parent constructor
|
|
16
|
+
super().__init__(event, context)
|
|
17
|
+
|
|
18
|
+
# ALB specific initialization
|
|
19
|
+
self.request_method = event.get("httpMethod", "GET").upper()
|
|
20
|
+
self.path = event.get("path", "/")
|
|
21
|
+
|
|
22
|
+
# Extract query parameters (ALB only has single value query parameters)
|
|
23
|
+
self.query_parameters = event.get("queryStringParameters") or {}
|
|
24
|
+
|
|
25
|
+
# Extract headers (ALB only has single value headers)
|
|
26
|
+
headers_dict = {}
|
|
27
|
+
for key, value in event.get("headers", {}).items():
|
|
28
|
+
headers_dict[key.lower()] = str(value)
|
|
29
|
+
|
|
30
|
+
self.request_headers = Headers(headers_dict)
|
|
31
|
+
|
|
32
|
+
def get_client_ip(self) -> str:
|
|
33
|
+
"""Get the client IP address from ALB headers."""
|
|
34
|
+
# ALB always provides client IP via X-Forwarded-For header
|
|
35
|
+
forwarded_for = self.request_headers.get("x-forwarded-for")
|
|
36
|
+
if not forwarded_for:
|
|
37
|
+
raise KeyError(
|
|
38
|
+
"The x-forwarded-for header wasn't present in the request, and it should always exist for anything behind an ALB. You are probably using the wrong context."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# X-Forwarded-For can contain multiple IPs, take the first one
|
|
42
|
+
return forwarded_for.split(",")[0].strip()
|
|
43
|
+
|
|
44
|
+
def context_specifics(self) -> dict[str, Any]:
|
|
45
|
+
"""Provide ALB specific context data."""
|
|
46
|
+
request_context = self.event.get("requestContext", {})
|
|
47
|
+
elb = request_context.get("elb", {})
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
**super().context_specifics(),
|
|
51
|
+
"path": self.path,
|
|
52
|
+
"target_group_arn": elb.get("targetGroupArn"),
|
|
53
|
+
}
|