Skip to content

Commit

Permalink
Events: create_connection() now creates a Secret with the right value (
Browse files Browse the repository at this point in the history
  • Loading branch information
bblommers authored Aug 15, 2024
1 parent 459be00 commit a0cd54c
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 47 deletions.
3 changes: 1 addition & 2 deletions moto/emr/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
ResourceNotFoundException,
ValidationException,
)
from moto.utilities.utils import get_partition
from moto.utilities.utils import CamelToUnderscoresWalker, get_partition

from .utils import (
CamelToUnderscoresWalker,
EmrSecurityGroupManager,
random_cluster_id,
random_instance_group_id,
Expand Down
36 changes: 1 addition & 35 deletions moto/emr/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import string
from typing import Any, Dict, Iterator, List, Tuple

from moto.core.utils import (
camelcase_to_underscores,
iso_8601_datetime_with_milliseconds,
)
from moto.core.utils import iso_8601_datetime_with_milliseconds
from moto.moto_api._internal import mock_random as random


Expand Down Expand Up @@ -129,37 +126,6 @@ def _key_in_container(container: Any, key: Any) -> bool: # type: ignore
return len(container) >= i


class CamelToUnderscoresWalker:
"""A class to convert the keys in dict/list hierarchical data structures from CamelCase to snake_case (underscores)"""

@staticmethod
def parse(x: Any) -> Any: # type: ignore[misc]
if isinstance(x, dict):
return CamelToUnderscoresWalker.parse_dict(x)
elif isinstance(x, list):
return CamelToUnderscoresWalker.parse_list(x)
else:
return CamelToUnderscoresWalker.parse_scalar(x)

@staticmethod
def parse_dict(x: Dict[str, Any]) -> Dict[str, Any]: # type: ignore[misc]
temp = {}
for key in x.keys():
temp[camelcase_to_underscores(key)] = CamelToUnderscoresWalker.parse(x[key])
return temp

@staticmethod
def parse_list(x: Any) -> Any: # type: ignore[misc]
temp = []
for i in x:
temp.append(CamelToUnderscoresWalker.parse(i))
return temp

@staticmethod
def parse_scalar(x: Any) -> Any: # type: ignore[misc]
return x


class ReleaseLabel:
version_re = re.compile(r"^emr-(\d+)\.(\d+)\.(\d+)$")

Expand Down
49 changes: 40 additions & 9 deletions moto/events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from enum import Enum, unique
from json import JSONDecodeError
from operator import eq, ge, gt, le, lt
from typing import Any, Dict, List, Optional
from typing import TYPE_CHECKING, Any, Dict, List, Optional

import requests

Expand All @@ -29,18 +29,32 @@
ValidationException,
)
from moto.moto_api._internal import mock_random as random
from moto.secretsmanager import secretsmanager_backends
from moto.utilities.arns import parse_arn
from moto.utilities.paginator import paginate
from moto.utilities.tagging_service import TaggingService
from moto.utilities.utils import ARN_PARTITION_REGEX, get_partition
from moto.utilities.utils import (
ARN_PARTITION_REGEX,
CamelToUnderscoresWalker,
get_partition,
)

from .utils import _BASE_EVENT_MESSAGE, PAGINATION_MODEL, EventMessageType

if TYPE_CHECKING:
from moto.secretsmanager.models import SecretsManagerBackend

# Sentinel to signal the absence of a field for `Exists` pattern matching
UNDEFINED = object()


def get_secrets_manager_backend(
account_id: str, region: str
) -> "SecretsManagerBackend":
from moto.secretsmanager import secretsmanager_backends

return secretsmanager_backends[account_id][region]


class Rule(CloudFormationModel):
def __init__(
self,
Expand Down Expand Up @@ -768,15 +782,23 @@ def __init__(
self.state = "AUTHORIZED"

connection_id = f"{self.name}/{self.uuid}"
secretsmanager_backend = secretsmanager_backends[account_id][region_name]
secretsmanager_backend = get_secrets_manager_backend(account_id, region_name)
secret_value = {}
for key, value in self.auth_parameters.items():
if key == "InvocationHttpParameters":
secret_value.update({"InvocationHttpParameters": value})
else:
secret_value.update(value)
secret_value = CamelToUnderscoresWalker.parse(secret_value)

secret = secretsmanager_backend.create_secret(
name=f"{connection_id}/auth",
secret_string=json.dumps(self.auth_parameters),
replica_regions=list(),
name=f"events!connection/{connection_id}/auth",
secret_string=json.dumps(secret_value),
replica_regions=[],
force_overwrite=False,
secret_binary=None,
description=f"Auth parameters for Eventbridge connection {connection_id}",
tags=list(),
tags=[{"Key": "aws:secretsmanager:owningService", "Value": "events"}],
kms_key_id=None,
client_request_token=None,
)
Expand Down Expand Up @@ -817,7 +839,6 @@ def describe(self) -> Dict[str, Any]:
- The original response also has:
- LastAuthorizedTime (number)
- LastModifiedTime (number)
- SecretArn (string)
- StateReason (string)
- At the time of implementing this, there was no place where to set/get
those attributes. That is why they are not in the response.
Expand Down Expand Up @@ -1842,6 +1863,16 @@ def delete_connection(self, name: str) -> Dict[str, Any]:
if not connection:
raise ResourceNotFoundException(f"Connection '{name}' does not exist.")

# Delete Secret
secretsmanager_backend = get_secrets_manager_backend(
self.account_id, self.region_name
)
secretsmanager_backend.delete_secret(
secret_id=connection.secret_arn,
recovery_window_in_days=None,
force_delete_without_recovery=True,
)

return connection.describe_short()

def create_api_destination(
Expand Down
2 changes: 1 addition & 1 deletion moto/secretsmanager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,7 @@ def list_secrets(
def delete_secret(
self,
secret_id: str,
recovery_window_in_days: int,
recovery_window_in_days: Optional[int],
force_delete_without_recovery: bool,
) -> Tuple[str, str, float]:
if recovery_window_in_days is not None and (
Expand Down
33 changes: 33 additions & 0 deletions moto/utilities/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,36 @@ def __repr__(self) -> str:

def _keytransform(self, key: str) -> str:
return key.lower()


class CamelToUnderscoresWalker:
"""A class to convert the keys in dict/list hierarchical data structures from CamelCase to snake_case (underscores)"""

@staticmethod
def parse(x: Any) -> Any: # type: ignore[misc]
if isinstance(x, dict):
return CamelToUnderscoresWalker.parse_dict(x)
elif isinstance(x, list):
return CamelToUnderscoresWalker.parse_list(x)
else:
return CamelToUnderscoresWalker.parse_scalar(x)

@staticmethod
def parse_dict(x: Dict[str, Any]) -> Dict[str, Any]: # type: ignore[misc]
from moto.core.utils import camelcase_to_underscores

temp = {}
for key in x.keys():
temp[camelcase_to_underscores(key)] = CamelToUnderscoresWalker.parse(x[key])
return temp

@staticmethod
def parse_list(x: Any) -> Any: # type: ignore[misc]
temp = []
for i in x:
temp.append(CamelToUnderscoresWalker.parse(i))
return temp

@staticmethod
def parse_scalar(x: Any) -> Any: # type: ignore[misc]
return x
92 changes: 92 additions & 0 deletions tests/test_events/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import unittest
import warnings
from datetime import datetime, timezone
from time import sleep
from uuid import uuid4

import boto3
import pytest
Expand All @@ -11,6 +13,7 @@
from moto import mock_aws, settings
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
from moto.core.utils import iso_8601_datetime_without_milliseconds
from tests import aws_verified

RULES = [
{"Name": "test1", "ScheduleExpression": "rate(5 minutes)"},
Expand Down Expand Up @@ -2312,6 +2315,95 @@ def test_create_and_update_connection():
assert "CreationTime" in description


@aws_verified
@pytest.mark.aws_verified
@pytest.mark.parametrize(
"auth_type,auth_parameters",
[
(
"API_KEY",
{"ApiKeyAuthParameters": {"ApiKeyName": "test", "ApiKeyValue": "test"}},
),
("BASIC", {"BasicAuthParameters": {"Username": "un", "Password": "pw"}}),
],
ids=["auth_params", "basic_auth_params"],
)
@pytest.mark.parametrize(
"with_headers", [True, False], ids=["with_headers", "without_headers"]
)
def test_kms_key_is_created(auth_type, auth_parameters, with_headers):
client = boto3.client("events", "us-east-1")
secrets = boto3.client("secretsmanager", "us-east-1")
sts = boto3.client("sts", "us-east-1")

name = f"event_{str(uuid4())[0:6]}"
account_id = sts.get_caller_identity()["Account"]
connection_deleted = False
if with_headers:
auth_parameters["InvocationHttpParameters"] = {
"HeaderParameters": [
{"Key": "k1", "Value": "v1", "IsValueSecret": True},
{"Key": "k2", "Value": "v2", "IsValueSecret": False},
]
}
else:
auth_parameters.pop("InvocationHttpParameters", None)

client.create_connection(
Name=name,
AuthorizationType=auth_type,
AuthParameters=auth_parameters,
)
try:
description = client.describe_connection(Name=name)
secret_arn = description["SecretArn"]
assert secret_arn.startswith(
f"arn:aws:secretsmanager:us-east-1:{account_id}:secret:events!connection/{name}/"
)

secret = secrets.describe_secret(SecretId=secret_arn)
assert secret["Name"].startswith(f"events!connection/{name}/")
assert secret["Tags"] == [
{"Key": "aws:secretsmanager:owningService", "Value": "events"}
]

if auth_type == "BASIC":
expected_secret = {"username": "un", "password": "pw"}
else:
expected_secret = {"api_key_name": "test", "api_key_value": "test"}
if with_headers:
expected_secret["invocation_http_parameters"] = {
"header_parameters": [
{"key": "k1", "value": "v1", "is_value_secret": True},
{"key": "k2", "value": "v2", "is_value_secret": False},
]
}

secret_value = secrets.get_secret_value(SecretId=secret_arn)
assert json.loads(secret_value["SecretString"]) == expected_secret

client.delete_connection(Name=name)
connection_deleted = True

secret_deleted = False
attempts = 0
while not secret_deleted and attempts < 5:
try:
attempts += 1
secrets.describe_secret(SecretId=secret_arn)
sleep(1)
except ClientError as e:
secret_deleted = (
e.response["Error"]["Code"] == "ResourceNotFoundException"
)

if not secret_deleted:
assert False, f"Should have automatically deleted secret {secret_arn}"
finally:
if not connection_deleted:
client.delete_connection(Name=name)


@mock_aws
def test_update_unknown_connection():
client = boto3.client("events", "eu-north-1")
Expand Down

0 comments on commit a0cd54c

Please sign in to comment.