Added support for AWS Config Retention Configuration
- Added AWS Config Retention Configuration support - Also added S3 KMS Key ARN support for the Delivery Channel - Updated the supported functions page for Config
This commit is contained in:
parent
dc460a3258
commit
ae8f2a19c6
@ -62,7 +62,7 @@ config
|
||||
- [ ] delete_remediation_configuration
|
||||
- [ ] delete_remediation_exceptions
|
||||
- [ ] delete_resource_config
|
||||
- [ ] delete_retention_configuration
|
||||
- [X] delete_retention_configuration
|
||||
- [ ] delete_stored_query
|
||||
- [ ] deliver_config_snapshot
|
||||
- [ ] describe_aggregate_compliance_by_config_rules
|
||||
@ -91,7 +91,7 @@ config
|
||||
- [ ] describe_remediation_configurations
|
||||
- [ ] describe_remediation_exceptions
|
||||
- [ ] describe_remediation_execution_status
|
||||
- [ ] describe_retention_configurations
|
||||
- [X] describe_retention_configurations
|
||||
- [ ] get_aggregate_compliance_details_by_config_rule
|
||||
- [ ] get_aggregate_config_rule_compliance_summary
|
||||
- [ ] get_aggregate_conformance_pack_compliance_summary
|
||||
@ -179,7 +179,7 @@ config
|
||||
- [ ] put_remediation_configurations
|
||||
- [ ] put_remediation_exceptions
|
||||
- [ ] put_resource_config
|
||||
- [ ] put_retention_configuration
|
||||
- [X] put_retention_configuration
|
||||
- [ ] put_stored_query
|
||||
- [ ] select_aggregate_resource_config
|
||||
- [ ] select_resource_config
|
||||
|
@ -114,6 +114,14 @@ class InvalidS3KeyPrefixException(JsonRESTError):
|
||||
super().__init__("InvalidS3KeyPrefixException", message)
|
||||
|
||||
|
||||
class InvalidS3KmsKeyArnException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self) -> None:
|
||||
message = "The arn '' is not a valid kms key or alias arn."
|
||||
super().__init__("InvalidS3KmsKeyArnException", message)
|
||||
|
||||
|
||||
class InvalidSNSTopicARNException(JsonRESTError):
|
||||
"""We are *only* validating that there is value that is not '' here."""
|
||||
|
||||
@ -373,3 +381,13 @@ class MissingRequiredConfigRuleParameterException(JsonRESTError):
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__("ParamValidationError", message)
|
||||
|
||||
|
||||
class NoSuchRetentionConfigurationException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, name: str):
|
||||
message = (
|
||||
f"Cannot find retention configuration with the specified name '{name}'."
|
||||
)
|
||||
super().__init__("NoSuchRetentionConfigurationException", message)
|
||||
|
@ -18,6 +18,7 @@ from moto.config.exceptions import (
|
||||
InvalidDeliveryChannelNameException,
|
||||
NoSuchBucketException,
|
||||
InvalidS3KeyPrefixException,
|
||||
InvalidS3KmsKeyArnException,
|
||||
InvalidSNSTopicARNException,
|
||||
MaxNumberOfDeliveryChannelsExceededException,
|
||||
NoAvailableDeliveryChannelException,
|
||||
@ -44,6 +45,7 @@ from moto.config.exceptions import (
|
||||
MaxNumberOfConfigRulesExceededException,
|
||||
InsufficientPermissionsException,
|
||||
NoSuchConfigRuleException,
|
||||
NoSuchRetentionConfigurationException,
|
||||
ResourceInUseException,
|
||||
MissingRequiredConfigRuleParameterException,
|
||||
)
|
||||
@ -259,6 +261,7 @@ class ConfigDeliveryChannel(ConfigEmptyDictable):
|
||||
s3_bucket_name: str,
|
||||
prefix: Optional[str] = None,
|
||||
sns_arn: Optional[str] = None,
|
||||
s3_kms_key_arn: Optional[str] = None,
|
||||
snapshot_properties: Optional[ConfigDeliverySnapshotProperties] = None,
|
||||
):
|
||||
super().__init__()
|
||||
@ -266,9 +269,21 @@ class ConfigDeliveryChannel(ConfigEmptyDictable):
|
||||
self.name = name
|
||||
self.s3_bucket_name = s3_bucket_name
|
||||
self.s3_key_prefix = prefix
|
||||
self.s3_kms_key_arn = s3_kms_key_arn
|
||||
self.sns_topic_arn = sns_arn
|
||||
self.config_snapshot_delivery_properties = snapshot_properties
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Need to override this function because the KMS Key ARN is written as `Arn` vs. SNS which is `ARN`."""
|
||||
data = super().to_dict()
|
||||
|
||||
# Fix the KMS ARN if it's here:
|
||||
kms_arn = data.pop("s3KmsKeyARN", None)
|
||||
if kms_arn:
|
||||
data["s3KmsKeyArn"] = kms_arn
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RecordingGroup(ConfigEmptyDictable):
|
||||
def __init__(
|
||||
@ -883,6 +898,14 @@ class ConfigRule(ConfigEmptyDictable):
|
||||
# Verify the rule is allowed for this region -- not yet implemented.
|
||||
|
||||
|
||||
class RetentionConfiguration(ConfigEmptyDictable):
|
||||
def __init__(self, retention_period_in_days: int, name: Optional[str] = None):
|
||||
super().__init__(capitalize_start=True, capitalize_arn=False)
|
||||
|
||||
self.name = name or "default"
|
||||
self.retention_period_in_days = retention_period_in_days
|
||||
|
||||
|
||||
class ConfigBackend(BaseBackend):
|
||||
def __init__(self, region_name: str, account_id: str):
|
||||
super().__init__(region_name, account_id)
|
||||
@ -893,6 +916,7 @@ class ConfigBackend(BaseBackend):
|
||||
self.organization_conformance_packs: Dict[str, OrganizationConformancePack] = {}
|
||||
self.config_rules: Dict[str, ConfigRule] = {}
|
||||
self.config_schema: Optional[AWSServiceSpec] = None
|
||||
self.retention_configuration: Optional[RetentionConfiguration] = None
|
||||
|
||||
@staticmethod
|
||||
def default_vpc_endpoint_service(service_region: str, zones: List[str]) -> List[Dict[str, Any]]: # type: ignore[misc]
|
||||
@ -1264,9 +1288,15 @@ class ConfigBackend(BaseBackend):
|
||||
|
||||
# Ditto for SNS -- Only going to assume that the ARN provided is not
|
||||
# an empty string:
|
||||
# NOTE: SNS "ARN" is all caps, but KMS "Arn" is UpperCamelCase!
|
||||
if delivery_channel.get("snsTopicARN", None) == "":
|
||||
raise InvalidSNSTopicARNException()
|
||||
|
||||
# Ditto for S3 KMS Key ARN -- Only going to assume that the ARN provided is not
|
||||
# an empty string:
|
||||
if delivery_channel.get("s3KmsKeyArn", None) == "":
|
||||
raise InvalidS3KmsKeyArnException()
|
||||
|
||||
# Config currently only allows 1 delivery channel for an account:
|
||||
if len(self.delivery_channels) == 1 and not self.delivery_channels.get(
|
||||
delivery_channel["name"]
|
||||
@ -1292,6 +1322,7 @@ class ConfigBackend(BaseBackend):
|
||||
delivery_channel["name"],
|
||||
delivery_channel["s3BucketName"],
|
||||
prefix=delivery_channel.get("s3KeyPrefix", None),
|
||||
s3_kms_key_arn=delivery_channel.get("s3KmsKeyArn", None),
|
||||
sns_arn=delivery_channel.get("snsTopicARN", None),
|
||||
snapshot_properties=dprop,
|
||||
)
|
||||
@ -2012,5 +2043,66 @@ class ConfigBackend(BaseBackend):
|
||||
rule.config_rule_state = "DELETING"
|
||||
self.config_rules.pop(rule_name)
|
||||
|
||||
def put_retention_configuration(
|
||||
self, retention_period_in_days: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Creates a Retention Configuration."""
|
||||
if retention_period_in_days < 30:
|
||||
raise ValidationException(
|
||||
f"Value '{retention_period_in_days}' at 'retentionPeriodInDays' failed to satisfy constraint: Member must have value greater than or equal to 30"
|
||||
)
|
||||
|
||||
if retention_period_in_days > 2557:
|
||||
raise ValidationException(
|
||||
f"Value '{retention_period_in_days}' at 'retentionPeriodInDays' failed to satisfy constraint: Member must have value less than or equal to 2557"
|
||||
)
|
||||
|
||||
self.retention_configuration = RetentionConfiguration(retention_period_in_days)
|
||||
return {"RetentionConfiguration": self.retention_configuration.to_dict()}
|
||||
|
||||
def describe_retention_configurations(
|
||||
self, retention_configuration_names: Optional[List[str]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
This will return the retention configuration if one is present.
|
||||
|
||||
This should only receive at most 1 name in. It will raise a ValidationException if more than 1 is supplied.
|
||||
"""
|
||||
# Handle the cases where we get a retention name to search for:
|
||||
if retention_configuration_names:
|
||||
if len(retention_configuration_names) > 1:
|
||||
raise ValidationException(
|
||||
f"Value '{retention_configuration_names}' at 'retentionConfigurationNames' failed to satisfy constraint: Member must have length less than or equal to 1"
|
||||
)
|
||||
|
||||
# If we get a retention name to search for, and we don't have it, then we need to raise an exception:
|
||||
if (
|
||||
not self.retention_configuration
|
||||
or not self.retention_configuration.name
|
||||
== retention_configuration_names[0]
|
||||
):
|
||||
raise NoSuchRetentionConfigurationException(
|
||||
retention_configuration_names[0]
|
||||
)
|
||||
|
||||
# If we found it, then return it:
|
||||
return [self.retention_configuration.to_dict()]
|
||||
|
||||
# If no name was supplied:
|
||||
if self.retention_configuration:
|
||||
return [self.retention_configuration.to_dict()]
|
||||
|
||||
return []
|
||||
|
||||
def delete_retention_configuration(self, retention_configuration_name: str) -> None:
|
||||
"""This will delete the retention configuration if one is present with the provided name."""
|
||||
if (
|
||||
not self.retention_configuration
|
||||
or not self.retention_configuration.name == retention_configuration_name
|
||||
):
|
||||
raise NoSuchRetentionConfigurationException(retention_configuration_name)
|
||||
|
||||
self.retention_configuration = None
|
||||
|
||||
|
||||
config_backends = BackendDict(ConfigBackend, "config")
|
||||
|
@ -235,3 +235,24 @@ class ConfigResponse(BaseResponse):
|
||||
def delete_config_rule(self) -> str:
|
||||
self.config_backend.delete_config_rule(self._get_param("ConfigRuleName"))
|
||||
return ""
|
||||
|
||||
def put_retention_configuration(self) -> str:
|
||||
retention_configuration = self.config_backend.put_retention_configuration(
|
||||
self._get_param("RetentionPeriodInDays")
|
||||
)
|
||||
return json.dumps(retention_configuration)
|
||||
|
||||
def describe_retention_configurations(self) -> str:
|
||||
retention_configurations = (
|
||||
self.config_backend.describe_retention_configurations(
|
||||
self._get_param("RetentionConfigurationNames")
|
||||
)
|
||||
)
|
||||
schema = {"RetentionConfigurations": retention_configurations}
|
||||
return json.dumps(schema)
|
||||
|
||||
def delete_retention_configuration(self) -> str:
|
||||
self.config_backend.delete_retention_configuration(
|
||||
self._get_param("RetentionConfigurationName")
|
||||
)
|
||||
return ""
|
||||
|
@ -4,6 +4,7 @@ import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
from botocore.exceptions import ClientError
|
||||
from unittest import SkipTest
|
||||
import pytest
|
||||
@ -879,6 +880,21 @@ def test_delivery_channels():
|
||||
assert ce.value.response["Error"]["Code"] == "InvalidSNSTopicARNException"
|
||||
assert "The sns topic arn" in ce.value.response["Error"]["Message"]
|
||||
|
||||
# With an empty string for the S3 KMS Key ARN:
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.put_delivery_channel(
|
||||
DeliveryChannel={
|
||||
"name": "testchannel",
|
||||
"s3BucketName": "somebucket",
|
||||
"s3KmsKeyArn": "",
|
||||
}
|
||||
)
|
||||
assert ce.value.response["Error"]["Code"] == "InvalidS3KmsKeyArnException"
|
||||
assert (
|
||||
ce.value.response["Error"]["Message"]
|
||||
== "The arn '' is not a valid kms key or alias arn."
|
||||
)
|
||||
|
||||
# With an invalid delivery frequency:
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.put_delivery_channel(
|
||||
@ -973,6 +989,7 @@ def test_describe_delivery_channels():
|
||||
"name": "testchannel",
|
||||
"s3BucketName": "somebucket",
|
||||
"snsTopicARN": "sometopicarn",
|
||||
"s3KmsKeyArn": "somekmsarn",
|
||||
"configSnapshotDeliveryProperties": {
|
||||
"deliveryFrequency": "TwentyFour_Hours"
|
||||
},
|
||||
@ -980,10 +997,11 @@ def test_describe_delivery_channels():
|
||||
)
|
||||
result = client.describe_delivery_channels()["DeliveryChannels"]
|
||||
assert len(result) == 1
|
||||
assert len(result[0].keys()) == 4
|
||||
assert len(result[0].keys()) == 5
|
||||
assert result[0]["name"] == "testchannel"
|
||||
assert result[0]["s3BucketName"] == "somebucket"
|
||||
assert result[0]["snsTopicARN"] == "sometopicarn"
|
||||
assert result[0]["s3KmsKeyArn"] == "somekmsarn"
|
||||
assert (
|
||||
result[0]["configSnapshotDeliveryProperties"]["deliveryFrequency"]
|
||||
== "TwentyFour_Hours"
|
||||
@ -2148,3 +2166,109 @@ def test_delete_organization_conformance_pack_errors():
|
||||
ex.response["Error"]["Message"].should.equal(
|
||||
"Could not find an OrganizationConformancePack for given request with resourceName not-existing"
|
||||
)
|
||||
|
||||
|
||||
@mock_config
|
||||
def test_put_retention_configuration():
|
||||
# Test with parameter validation being False to test the retention days check:
|
||||
client = boto3.client(
|
||||
"config",
|
||||
region_name="us-west-2",
|
||||
config=Config(parameter_validation=False, user_agent_extra="moto"),
|
||||
)
|
||||
|
||||
# Less than 30 days:
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.put_retention_configuration(RetentionPeriodInDays=29)
|
||||
assert (
|
||||
ce.value.response["Error"]["Message"]
|
||||
== "Value '29' at 'retentionPeriodInDays' failed to satisfy constraint: Member must have value greater than or equal to 30"
|
||||
)
|
||||
|
||||
# More than 2557 days:
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.put_retention_configuration(RetentionPeriodInDays=2558)
|
||||
assert (
|
||||
ce.value.response["Error"]["Message"]
|
||||
== "Value '2558' at 'retentionPeriodInDays' failed to satisfy constraint: Member must have value less than or equal to 2557"
|
||||
)
|
||||
|
||||
# No errors:
|
||||
result = client.put_retention_configuration(RetentionPeriodInDays=2557)
|
||||
assert result["RetentionConfiguration"] == {
|
||||
"Name": "default",
|
||||
"RetentionPeriodInDays": 2557,
|
||||
}
|
||||
|
||||
|
||||
@mock_config
|
||||
def test_describe_retention_configurations():
|
||||
client = boto3.client("config", region_name="us-west-2")
|
||||
|
||||
# Without any recorder configurations:
|
||||
result = client.describe_retention_configurations()
|
||||
assert not result["RetentionConfigurations"]
|
||||
|
||||
# Create one and then describe:
|
||||
client.put_retention_configuration(RetentionPeriodInDays=2557)
|
||||
result = client.describe_retention_configurations()
|
||||
assert result["RetentionConfigurations"] == [
|
||||
{"Name": "default", "RetentionPeriodInDays": 2557}
|
||||
]
|
||||
|
||||
# Describe with the correct name:
|
||||
result = client.describe_retention_configurations(
|
||||
RetentionConfigurationNames=["default"]
|
||||
)
|
||||
assert result["RetentionConfigurations"] == [
|
||||
{"Name": "default", "RetentionPeriodInDays": 2557}
|
||||
]
|
||||
|
||||
# Describe with more than 1 configuration name provided:
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.describe_retention_configurations(
|
||||
RetentionConfigurationNames=["bad", "entry"]
|
||||
)
|
||||
assert (
|
||||
ce.value.response["Error"]["Message"]
|
||||
== "Value '['bad', 'entry']' at 'retentionConfigurationNames' failed to satisfy constraint: "
|
||||
"Member must have length less than or equal to 1"
|
||||
)
|
||||
|
||||
# Describe with a name that's not default:
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.describe_retention_configurations(
|
||||
RetentionConfigurationNames=["notfound"]
|
||||
)
|
||||
assert (
|
||||
ce.value.response["Error"]["Message"]
|
||||
== "Cannot find retention configuration with the specified name 'notfound'."
|
||||
)
|
||||
|
||||
|
||||
@mock_config
|
||||
def test_delete_retention_configuration():
|
||||
client = boto3.client("config", region_name="us-west-2")
|
||||
|
||||
# Create one first:
|
||||
client.put_retention_configuration(RetentionPeriodInDays=2557)
|
||||
|
||||
# Delete with a name that's not default:
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.delete_retention_configuration(RetentionConfigurationName="notfound")
|
||||
assert (
|
||||
ce.value.response["Error"]["Message"]
|
||||
== "Cannot find retention configuration with the specified name 'notfound'."
|
||||
)
|
||||
|
||||
# Delete it properly:
|
||||
client.delete_retention_configuration(RetentionConfigurationName="default")
|
||||
assert not client.describe_retention_configurations()["RetentionConfigurations"]
|
||||
|
||||
# And again...
|
||||
with pytest.raises(ClientError) as ce:
|
||||
client.delete_retention_configuration(RetentionConfigurationName="default")
|
||||
assert (
|
||||
ce.value.response["Error"]["Message"]
|
||||
== "Cannot find retention configuration with the specified name 'default'."
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user