Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events: create_connection() now creates a Secret with the right value #7982

Merged
merged 1 commit into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading