|
2 | 2 | AWS Secrets Manager parameter retrieval and caching utility
|
3 | 3 | """
|
4 | 4 |
|
| 5 | +from __future__ import annotations |
| 6 | + |
| 7 | +import json |
| 8 | +import logging |
5 | 9 | import os
|
6 | 10 | from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union, overload
|
7 | 11 |
|
8 | 12 | import boto3
|
9 | 13 | from botocore.config import Config
|
10 | 14 |
|
11 |
| -from aws_lambda_powertools.utilities.parameters.types import TransformOptions |
12 |
| - |
13 | 15 | if TYPE_CHECKING:
|
14 | 16 | from mypy_boto3_secretsmanager import SecretsManagerClient
|
15 | 17 |
|
16 | 18 | from aws_lambda_powertools.shared import constants
|
17 | 19 | from aws_lambda_powertools.shared.functions import resolve_max_age
|
| 20 | +from aws_lambda_powertools.shared.json_encoder import Encoder |
| 21 | +from aws_lambda_powertools.utilities.parameters.base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider |
| 22 | +from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError |
| 23 | +from aws_lambda_powertools.utilities.parameters.types import SetSecretResponse, TransformOptions |
18 | 24 |
|
19 |
| -from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider |
| 25 | +logger = logging.getLogger(__name__) |
20 | 26 |
|
21 | 27 |
|
22 | 28 | class SecretsProvider(BaseProvider):
|
@@ -117,6 +123,134 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:
|
117 | 123 | """
|
118 | 124 | raise NotImplementedError()
|
119 | 125 |
|
| 126 | + def _create_secret(self, name: str, **sdk_options): |
| 127 | + """ |
| 128 | + Create a secret with the given name. |
| 129 | +
|
| 130 | + Parameters: |
| 131 | + ---------- |
| 132 | + name: str |
| 133 | + The name of the secret. |
| 134 | + **sdk_options: |
| 135 | + Additional options to be passed to the create_secret method. |
| 136 | +
|
| 137 | + Raises: |
| 138 | + SetSecretError: If there is an error setting the secret. |
| 139 | + """ |
| 140 | + try: |
| 141 | + sdk_options["Name"] = name |
| 142 | + return self.client.create_secret(**sdk_options) |
| 143 | + except Exception as exc: |
| 144 | + raise SetSecretError(f"Error setting secret - {str(exc)}") from exc |
| 145 | + |
| 146 | + def _update_secret(self, name: str, **sdk_options): |
| 147 | + """ |
| 148 | + Update a secret with the given name. |
| 149 | +
|
| 150 | + Parameters: |
| 151 | + ---------- |
| 152 | + name: str |
| 153 | + The name of the secret. |
| 154 | + **sdk_options: |
| 155 | + Additional options to be passed to the create_secret method. |
| 156 | + """ |
| 157 | + sdk_options["SecretId"] = name |
| 158 | + return self.client.put_secret_value(**sdk_options) |
| 159 | + |
| 160 | + def set( |
| 161 | + self, |
| 162 | + name: str, |
| 163 | + value: Union[str, dict, bytes], |
| 164 | + *, # force keyword arguments |
| 165 | + client_request_token: Optional[str] = None, |
| 166 | + **sdk_options, |
| 167 | + ) -> SetSecretResponse: |
| 168 | + """ |
| 169 | + Modify the details of a secret or create a new secret if it doesn't already exist. |
| 170 | +
|
| 171 | + We aim to minimize API calls by assuming that the secret already exists and needs updating. |
| 172 | + If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding: |
| 173 | +
|
| 174 | +
|
| 175 | + ┌────────────────────────┐ ┌─────────────────┐ |
| 176 | + ┌───────▶│Resource NotFound error?│────▶│Create Secret API│─────┐ |
| 177 | + │ └────────────────────────┘ └─────────────────┘ │ |
| 178 | + │ │ |
| 179 | + │ │ |
| 180 | + │ ▼ |
| 181 | + ┌─────────────────┐ ┌─────────────────────┐ |
| 182 | + │Update Secret API│────────────────────────────────────────────▶│ Return or Exception │ |
| 183 | + └─────────────────┘ └─────────────────────┘ |
| 184 | +
|
| 185 | + Parameters |
| 186 | + ---------- |
| 187 | + name: str |
| 188 | + The ARN or name of the secret to add a new version to or create a new one. |
| 189 | + value: str, dict or bytes |
| 190 | + Specifies text data that you want to encrypt and store in this new version of the secret. |
| 191 | + client_request_token: str, optional |
| 192 | + This value helps ensure idempotency. It's recommended that you generate |
| 193 | + a UUID-type value to ensure uniqueness within the specified secret. |
| 194 | + This value becomes the VersionId of the new version. This field is |
| 195 | + auto-populated if not provided, but no idempotency will be enforced this way. |
| 196 | + sdk_options: dict, optional |
| 197 | + Dictionary of options that will be passed to the Secrets Manager update_secret API call |
| 198 | +
|
| 199 | + Raises |
| 200 | + ------ |
| 201 | + SetSecretError |
| 202 | + When attempting to update or create a secret fails. |
| 203 | +
|
| 204 | + Returns: |
| 205 | + ------- |
| 206 | + SetSecretResponse: |
| 207 | + The dict returned by boto3. |
| 208 | +
|
| 209 | + Example |
| 210 | + ------- |
| 211 | + **Sets a secret*** |
| 212 | +
|
| 213 | + >>> from aws_lambda_powertools.utilities import parameters |
| 214 | + >>> |
| 215 | + >>> parameters.set_secret(name="llamas-are-awesome", value="supers3cr3tllam@passw0rd") |
| 216 | +
|
| 217 | + **Sets a secret and includes an client_request_token** |
| 218 | +
|
| 219 | + >>> from aws_lambda_powertools.utilities import parameters |
| 220 | + >>> import uuid |
| 221 | + >>> |
| 222 | + >>> parameters.set_secret( |
| 223 | + name="my-secret", |
| 224 | + value='{"password": "supers3cr3tllam@passw0rd"}', |
| 225 | + client_request_token=str(uuid.uuid4()) |
| 226 | + ) |
| 227 | +
|
| 228 | + URLs: |
| 229 | + ------- |
| 230 | + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html |
| 231 | + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/create_secret.html |
| 232 | + """ |
| 233 | + |
| 234 | + if isinstance(value, dict): |
| 235 | + value = json.dumps(value, cls=Encoder) |
| 236 | + |
| 237 | + if isinstance(value, bytes): |
| 238 | + sdk_options["SecretBinary"] = value |
| 239 | + else: |
| 240 | + sdk_options["SecretString"] = value |
| 241 | + |
| 242 | + if client_request_token: |
| 243 | + sdk_options["ClientRequestToken"] = client_request_token |
| 244 | + |
| 245 | + try: |
| 246 | + logger.debug(f"Attempting to update secret {name}") |
| 247 | + return self._update_secret(name=name, **sdk_options) |
| 248 | + except self.client.exceptions.ResourceNotFoundException: |
| 249 | + logger.debug(f"Secret {name} doesn't exist, creating a new one") |
| 250 | + return self._create_secret(name=name, **sdk_options) |
| 251 | + except Exception as exc: |
| 252 | + raise SetSecretError(f"Error setting secret - {str(exc)}") from exc |
| 253 | + |
120 | 254 |
|
121 | 255 | @overload
|
122 | 256 | def get_secret(
|
@@ -224,3 +358,87 @@ def get_secret(
|
224 | 358 | force_fetch=force_fetch,
|
225 | 359 | **sdk_options,
|
226 | 360 | )
|
| 361 | + |
| 362 | + |
| 363 | +def set_secret( |
| 364 | + name: str, |
| 365 | + value: Union[str, bytes], |
| 366 | + *, # force keyword arguments |
| 367 | + client_request_token: Optional[str] = None, |
| 368 | + **sdk_options, |
| 369 | +) -> SetSecretResponse: |
| 370 | + """ |
| 371 | + Modify the details of a secret or create a new secret if it doesn't already exist. |
| 372 | +
|
| 373 | + We aim to minimize API calls by assuming that the secret already exists and needs updating. |
| 374 | + If it doesn't exist, we attempt to create a new one. Refer to the following workflow for a better understanding: |
| 375 | +
|
| 376 | +
|
| 377 | + ┌────────────────────────┐ ┌─────────────────┐ |
| 378 | + ┌───────▶│Resource NotFound error?│────▶│Create Secret API│─────┐ |
| 379 | + │ └────────────────────────┘ └─────────────────┘ │ |
| 380 | + │ │ |
| 381 | + │ │ |
| 382 | + │ ▼ |
| 383 | + ┌─────────────────┐ ┌─────────────────────┐ |
| 384 | + │Update Secret API│────────────────────────────────────────────▶│ Return or Exception │ |
| 385 | + └─────────────────┘ └─────────────────────┘ |
| 386 | +
|
| 387 | + Parameters |
| 388 | + ---------- |
| 389 | + name: str |
| 390 | + The ARN or name of the secret to add a new version to or create a new one. |
| 391 | + value: str, dict or bytes |
| 392 | + Specifies text data that you want to encrypt and store in this new version of the secret. |
| 393 | + client_request_token: str, optional |
| 394 | + This value helps ensure idempotency. It's recommended that you generate |
| 395 | + a UUID-type value to ensure uniqueness within the specified secret. |
| 396 | + This value becomes the VersionId of the new version. This field is |
| 397 | + auto-populated if not provided, but no idempotency will be enforced this way. |
| 398 | + sdk_options: dict, optional |
| 399 | + Dictionary of options that will be passed to the Secrets Manager update_secret API call |
| 400 | +
|
| 401 | + Raises |
| 402 | + ------ |
| 403 | + SetSecretError |
| 404 | + When attempting to update or create a secret fails. |
| 405 | +
|
| 406 | + Returns: |
| 407 | + ------- |
| 408 | + SetSecretResponse: |
| 409 | + The dict returned by boto3. |
| 410 | +
|
| 411 | + Example |
| 412 | + ------- |
| 413 | + **Sets a secret*** |
| 414 | +
|
| 415 | + >>> from aws_lambda_powertools.utilities import parameters |
| 416 | + >>> |
| 417 | + >>> parameters.set_secret(name="llamas-are-awesome", value="supers3cr3tllam@passw0rd") |
| 418 | +
|
| 419 | + **Sets a secret and includes an client_request_token** |
| 420 | +
|
| 421 | + >>> from aws_lambda_powertools.utilities import parameters |
| 422 | + >>> |
| 423 | + >>> parameters.set_secret( |
| 424 | + name="my-secret", |
| 425 | + value='{"password": "supers3cr3tllam@passw0rd"}', |
| 426 | + client_request_token="61f2af5f-5f75-44b1-a29f-0cc37af55b11" |
| 427 | + ) |
| 428 | +
|
| 429 | + URLs: |
| 430 | + ------- |
| 431 | + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/put_secret_value.html |
| 432 | + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager/client/create_secret.html |
| 433 | + """ |
| 434 | + |
| 435 | + # Only create the provider if this function is called at least once |
| 436 | + if "secrets" not in DEFAULT_PROVIDERS: |
| 437 | + DEFAULT_PROVIDERS["secrets"] = SecretsProvider() |
| 438 | + |
| 439 | + return DEFAULT_PROVIDERS["secrets"].set( |
| 440 | + name=name, |
| 441 | + value=value, |
| 442 | + client_request_token=client_request_token, |
| 443 | + **sdk_options, |
| 444 | + ) |
0 commit comments