diff --git a/acceptance/run.sh b/acceptance/run.sh index b281bcc3..9b305ae1 100755 --- a/acceptance/run.sh +++ b/acceptance/run.sh @@ -154,6 +154,13 @@ tests() { "$PLAN_NAME" \ "$INSTANCE" \ -c "{\"domains\": \"$DOMAIN_0, $DOMAIN_1\", \"forward_cookies\": \"cookieone, cookietwo\", \"forward_headers\": \"x-one-header,x-two-header\", \"error_responses\": {\"404\": \"/errors/404.html\"}}" + elif [[ "${PLAN_NAME}" == "domain-with-cdn-dedicated-wafP" ]]; then + echo "Creating the service instance" + cf create-service \ + "$SERVICE_NAME" \ + "$PLAN_NAME" \ + "$INSTANCE" \ + -c "{\"domains\": \"$DOMAIN_0, $DOMAIN_1\", \"alarm_notification_email\": \"$ALARM_NOTIFICATION_EMAIL\", \"forward_cookies\": \"cookieone, cookietwo\", \"forward_headers\": \"x-one-header,x-two-header\", \"error_responses\": {\"404\": \"/errors/404.html\"}}" else echo "Creating the service instance" cf create-service \ diff --git a/broker/api.py b/broker/api.py index b87ea454..2ab31cfa 100644 --- a/broker/api.py +++ b/broker/api.py @@ -25,7 +25,7 @@ from broker import validators from broker.extensions import config, db -from broker.lib.cdn import is_cdn_instance +from broker.lib.cdn import is_cdn_instance, is_cdn_dedicated_waf_instance from broker.lib.tags import generate_instance_tags from broker.models import ( Operation, @@ -68,6 +68,7 @@ queue_all_migration_deprovision_tasks_for_operation, ) from broker.lib.utils import ( + parse_alarm_notification_email, parse_cookie_options, parse_header_options, normalize_header_list, @@ -98,7 +99,7 @@ def catalog(self) -> Service: imageUrl="TODO", longDescription="Create a custom domain to your application with TLS and an optional CDN. This will provision a TLS certificate from Let's Encrypt, a free certificate provider.", providerDisplayName="Cloud.gov", - documentationUrl="https://github.com/cloud-gov/external-domain-broker", + documentationUrl="https://cloud.gov/docs/services/external-domain-service/", supportUrl="https://cloud.gov/support", ), plans=[ @@ -336,16 +337,17 @@ def update( # noqa C901 # TODO: simplify this function elif details.plan_id == CDN_DEDICATED_WAF_PLAN_ID: queue = queue_all_cdn_to_cdn_dedicated_waf_update_tasks_for_operation - # update and commit any changes to the instance before changing its type, + # commit any changes to the instance before changing its type, # which will wipe out any pending changes on `instance` - instance = update_cdn_instance(params, instance) db.session.add(instance) db.session.commit() instance = change_instance_type( instance, CDNDedicatedWAFServiceInstance, db.session ) - db.session.refresh(instance) + instance = update_cdn_instance(params, instance) + db.session.add(instance) + db.session.commit() else: raise ClientError("Updating service plan is not supported") elif instance.instance_type == ServiceInstanceTypes.CDN_DEDICATED_WAF.value: @@ -471,6 +473,15 @@ def provision_cdn_instance( instance.origin_protocol_policy = instance_type_model.ProtocolPolicy.HTTP.value else: instance.origin_protocol_policy = instance_type_model.ProtocolPolicy.HTTPS.value + + alarm_notification_email = parse_alarm_notification_email(instance, params) + if alarm_notification_email: + instance.alarm_notification_email = alarm_notification_email + elif is_cdn_dedicated_waf_instance(instance) and not alarm_notification_email: + raise errors.ErrBadRequest( + f"'alarm_notification_email' is required for {ServiceInstanceTypes.CDN_DEDICATED_WAF.value} instances" + ) + return instance @@ -520,6 +531,17 @@ def update_cdn_instance(params, instance): instance.error_responses = params["error_responses"] validators.ErrorResponseConfig(instance.error_responses).validate() + alarm_notification_email = parse_alarm_notification_email(instance, params) + if alarm_notification_email: + instance.alarm_notification_email = alarm_notification_email + elif ( + is_cdn_dedicated_waf_instance(instance) + and not instance.alarm_notification_email + ): + raise errors.ErrBadRequest( + f"'alarm_notification_email' is required for {ServiceInstanceTypes.CDN_DEDICATED_WAF.value}" + ) + return instance diff --git a/broker/aws.py b/broker/aws.py index 26155bcc..d5929759 100644 --- a/broker/aws.py +++ b/broker/aws.py @@ -21,6 +21,7 @@ ) wafv2 = commercial_global_session.client("wafv2") cloudwatch_commercial = commercial_global_session.client("cloudwatch") +sns_commercial = commercial_global_session.client("sns") govcloud_session = boto3.Session( region_name=config.AWS_GOVCLOUD_REGION, diff --git a/broker/config.py b/broker/config.py index 98746510..e7517e81 100644 --- a/broker/config.py +++ b/broker/config.py @@ -56,8 +56,7 @@ def __init__(self): # the maximum from AWS is 25, and we alert when we have 20 on a given alb self.MAX_CERTS_PER_ALB = 19 - self.DEDICATED_WAF_NAME_PREFIX = f"cg-external-domains-{self.FLASK_ENV}" - self.CLOUDWATCH_ALARM_NAME_PREFIX = f"cg-external-domains-{self.FLASK_ENV}" + self.AWS_RESOURCE_PREFIX = f"cg-external-domains-{self.FLASK_ENV}" # see https://requests.readthedocs.io/en/latest/user/advanced/#timeouts self.REQUEST_TIMEOUT = 30 @@ -134,8 +133,6 @@ def __init__(self): self.UAA_CLIENT_ID = self.env("UAA_CLIENT_ID") self.UAA_CLIENT_SECRET = self.env("UAA_CLIENT_SECRET") - self.NOTIFICATIONS_SNS_TOPIC_ARN = self.env("NOTIFICATIONS_SNS_TOPIC_ARN") - class ProductionConfig(AppConfig): def __init__(self): @@ -185,7 +182,6 @@ def __init__(self): self.ALB_LISTENER_ARNS = [] self.DEDICATED_ALB_LISTENER_ARNS = [] self.WAF_RATE_LIMIT_RULE_GROUP_ARN = "NONE" - self.NOTIFICATIONS_SNS_TOPIC_ARN = "NONE" class CheckDuplicateCertsConfig(UpgradeSchemaConfig): @@ -262,8 +258,6 @@ def __init__(self): self.UAA_CLIENT_ID = "EXAMPLE" self.UAA_CLIENT_SECRET = "example" - self.NOTIFICATIONS_SNS_TOPIC_ARN = "fake-notifications-arn" - class LocalDevelopmentConfig(DockerConfig): def __init__(self): diff --git a/broker/lib/cdn.py b/broker/lib/cdn.py index c4b661ce..a9e283ed 100644 --- a/broker/lib/cdn.py +++ b/broker/lib/cdn.py @@ -1,8 +1,14 @@ from broker.models import ServiceInstanceTypes -def is_cdn_instance(service_instance): +def is_cdn_instance(service_instance) -> bool: return service_instance.instance_type in [ ServiceInstanceTypes.CDN.value, ServiceInstanceTypes.CDN_DEDICATED_WAF.value, ] + + +def is_cdn_dedicated_waf_instance(service_instance) -> bool: + return ( + service_instance.instance_type == ServiceInstanceTypes.CDN_DEDICATED_WAF.value + ) diff --git a/broker/lib/utils.py b/broker/lib/utils.py index 92a0d2f8..62919900 100644 --- a/broker/lib/utils.py +++ b/broker/lib/utils.py @@ -1,9 +1,9 @@ import logging from broker import validators -from broker.extensions import db -from broker.lib.cdn import is_cdn_instance +from broker.lib.cdn import is_cdn_dedicated_waf_instance from broker.models import ( + ServiceInstanceTypes, CDNServiceInstance, ) @@ -56,6 +56,13 @@ def parse_domain_options(params) -> list[str]: return [d.strip().lower() for d in domains] +def parse_alarm_notification_email(instance, params): + if not is_cdn_dedicated_waf_instance(instance): + return None + + return params.get("alarm_notification_email") + + def validate_domain_name_changes(requested_domain_names, instance) -> list[str]: if len(requested_domain_names) > 0: logger.info("validating CNAMEs") diff --git a/broker/models.py b/broker/models.py index 32e84a6b..94813a1e 100644 --- a/broker/models.py +++ b/broker/models.py @@ -192,6 +192,9 @@ class CDNDedicatedWAFServiceInstance(CDNServiceInstance): route53_health_checks = mapped_column(postgresql.JSONB, default=[]) shield_associated_health_check = mapped_column(postgresql.JSONB, default={}) cloudwatch_health_check_alarms = mapped_column(postgresql.JSONB, default=[]) + alarm_notification_email = mapped_column(db.String) + sns_notification_topic_arn = mapped_column(db.String) + ddos_detected_cloudwatch_alarm_name = mapped_column(db.String) __mapper_args__ = { "polymorphic_identity": ServiceInstanceTypes.CDN_DEDICATED_WAF.value diff --git a/broker/pipelines/cdn_dedicated_waf.py b/broker/pipelines/cdn_dedicated_waf.py index 092e38c1..43ac72b6 100644 --- a/broker/pipelines/cdn_dedicated_waf.py +++ b/broker/pipelines/cdn_dedicated_waf.py @@ -9,6 +9,7 @@ waf, shield, cloudwatch, + sns, ) from broker.tasks.huey import huey @@ -37,9 +38,11 @@ def queue_all_cdn_dedicated_waf_provision_tasks_for_operation( .then(cloudfront.wait_for_distribution, operation_id, **correlation) .then(route53.create_ALIAS_records, operation_id, **correlation) .then(route53.wait_for_changes, operation_id, **correlation) + .then(sns.create_notification_topic, operation_id, **correlation) .then(route53.create_new_health_checks, operation_id, **correlation) .then(shield.associate_health_check, operation_id, **correlation) .then(cloudwatch.create_health_check_alarms, operation_id, **correlation) + .then(cloudwatch.create_ddos_detected_alarm, operation_id, **correlation) .then(update_operations.provision, operation_id, **correlation) ) huey.enqueue(task_pipeline) @@ -57,9 +60,11 @@ def queue_all_cdn_dedicated_waf_deprovision_tasks_for_operation( update_operations.cancel_pending_provisioning.s(operation_id, **correlation) .then(route53.remove_ALIAS_records, operation_id, **correlation) .then(route53.remove_TXT_records, operation_id, **correlation) + .then(cloudwatch.delete_ddos_detected_alarm, operation_id, **correlation) .then(cloudwatch.delete_health_check_alarms, operation_id, **correlation) .then(shield.disassociate_health_check, operation_id, **correlation) .then(route53.delete_health_checks, operation_id, **correlation) + .then(sns.delete_notification_topic, operation_id, **correlation) .then(cloudfront.disable_distribution, operation_id, **correlation) .then(cloudfront.wait_for_distribution_disabled, operation_id, **correlation) .then(cloudfront.delete_distribution, operation_id=operation_id, **correlation) diff --git a/broker/pipelines/plan_updates.py b/broker/pipelines/plan_updates.py index 073bd45d..1bff2ffc 100644 --- a/broker/pipelines/plan_updates.py +++ b/broker/pipelines/plan_updates.py @@ -8,6 +8,7 @@ waf, shield, cloudwatch, + sns, ) from broker.tasks.huey import huey @@ -57,9 +58,11 @@ def queue_all_cdn_to_cdn_dedicated_waf_update_tasks_for_operation( .then(route53.create_ALIAS_records, operation_id, **correlation) .then(route53.wait_for_changes, operation_id, **correlation) .then(iam.delete_previous_server_certificate, operation_id, **correlation) + .then(sns.create_notification_topic, operation_id, **correlation) .then(route53.create_new_health_checks, operation_id, **correlation) .then(shield.associate_health_check, operation_id, **correlation) .then(cloudwatch.create_health_check_alarms, operation_id, **correlation) + .then(cloudwatch.create_ddos_detected_alarm, operation_id, **correlation) .then(update_operations.update_complete, operation_id, **correlation) ) huey.enqueue(task_pipeline) diff --git a/broker/tasks/cloudwatch.py b/broker/tasks/cloudwatch.py index a1a54b94..73cfc127 100644 --- a/broker/tasks/cloudwatch.py +++ b/broker/tasks/cloudwatch.py @@ -3,7 +3,7 @@ from botocore.exceptions import ClientError from sqlalchemy.orm.attributes import flag_modified -from broker.aws import cloudwatch_commercial +from broker.aws import cloudwatch_commercial, sns_commercial from broker.extensions import config, db @@ -23,6 +23,11 @@ def create_health_check_alarms(operation_id: int, **kwargs): db.session.add(operation) db.session.commit() + if not service_instance.sns_notification_topic_arn: + raise RuntimeError( + f"Could not find sns_notification_topic_arn for instance {service_instance.id}" + ) + if len(service_instance.route53_health_checks) == 0: logger.info( f"No Route53 health checks to create alarms on instance {service_instance.id}" @@ -30,7 +35,10 @@ def create_health_check_alarms(operation_id: int, **kwargs): return new_health_check_alarms = _create_health_check_alarms( - service_instance.route53_health_checks, [], service_instance.tags + service_instance.route53_health_checks, + [], + service_instance.sns_notification_topic_arn, + service_instance.tags, ) service_instance.cloudwatch_health_check_alarms = new_health_check_alarms flag_modified(service_instance, "cloudwatch_health_check_alarms") @@ -80,7 +88,7 @@ def update_health_check_alarms(operation_id: int, **kwargs): not in existing_route53_health_check_ids ] if len(health_checks_alarm_names_to_delete) > 0: - updated_health_check_alarms = _delete_alarms( + updated_health_check_alarms = _delete_cloudwatch_health_check_alarms( updated_health_check_alarms, health_checks_alarm_names_to_delete ) service_instance.cloudwatch_health_check_alarms = updated_health_check_alarms @@ -96,9 +104,15 @@ def update_health_check_alarms(operation_id: int, **kwargs): not in existing_cloudwatch_alarm_health_check_ids ] if len(health_checks_to_create_alarms) > 0: + if not service_instance.sns_notification_topic_arn: + raise RuntimeError( + f"Could not find sns_notification_topic_arn for instance {service_instance.id}" + ) + updated_health_check_alarms = _create_health_check_alarms( health_checks_to_create_alarms, updated_health_check_alarms, + service_instance.sns_notification_topic_arn, service_instance.tags, ) service_instance.cloudwatch_health_check_alarms = updated_health_check_alarms @@ -129,7 +143,7 @@ def delete_health_check_alarms(operation_id: int, **kwargs): ] if len(health_checks_alarm_names_to_delete) > 0: - updated_health_check_alarms = _delete_alarms( + updated_health_check_alarms = _delete_cloudwatch_health_check_alarms( existing_health_check_alarms, health_checks_alarm_names_to_delete ) service_instance.cloudwatch_health_check_alarms = updated_health_check_alarms @@ -139,12 +153,71 @@ def delete_health_check_alarms(operation_id: int, **kwargs): db.session.commit() +@huey.retriable_task +def create_ddos_detected_alarm(operation_id: int, **kwargs): + operation = db.session.get(Operation, operation_id) + service_instance = operation.service_instance + + operation.step_description = "Creating DDoS detection alarm" + flag_modified(operation, "step_description") + db.session.add(operation) + db.session.commit() + + if not service_instance.sns_notification_topic_arn: + raise RuntimeError( + f"Could not find sns_notification_topic_arn for instance {service_instance.id}" + ) + + ddos_detected_alarm_name = generate_ddos_alarm_name(service_instance.id) + _create_cloudwatch_alarm( + generate_ddos_alarm_name(service_instance.id), + service_instance.sns_notification_topic_arn, + service_instance.tags, + MetricName="DDoSDetected", + Namespace="AWS/DDoSProtection", + Statistic="Minimum", + Dimensions=[ + { + "Name": "ResourceArn", + "Value": service_instance.cloudfront_distribution_arn, + } + ], + ) + service_instance.ddos_detected_cloudwatch_alarm_name = ddos_detected_alarm_name + db.session.add(service_instance) + db.session.commit() + + +@huey.retriable_task +def delete_ddos_detected_alarm(operation_id: int, **kwargs): + operation = db.session.get(Operation, operation_id) + service_instance = operation.service_instance + + operation.step_description = "Deleting DDoS detection alarm" + flag_modified(operation, "step_description") + db.session.add(operation) + db.session.commit() + + if not service_instance.ddos_detected_cloudwatch_alarm_name: + return + + _delete_alarms([service_instance.ddos_detected_cloudwatch_alarm_name]) + service_instance.ddos_detected_cloudwatch_alarm_name = None + db.session.add(service_instance) + db.session.commit() + + def _create_health_check_alarms( - health_checks_to_create_alarms, existing_health_check_alarms, tags + health_checks_to_create_alarms, + existing_health_check_alarms, + sns_notification_topic_arn, + tags, ): for health_check in health_checks_to_create_alarms: health_check_id = health_check["health_check_id"] - alarm_name = _create_health_check_alarm(health_check_id, tags) + alarm_name = _create_health_check_alarm( + health_check_id, sns_notification_topic_arn, tags + ) existing_health_check_alarms.append( { @@ -155,17 +228,15 @@ def _create_health_check_alarms( return existing_health_check_alarms -def _create_health_check_alarm(health_check_id, tags) -> str: +def _create_health_check_alarm( + health_check_id, sns_notification_topic_arn, tags +) -> str: alarm_name = _get_alarm_name(health_check_id) - # create alarm - kwargs = {} - if tags: - kwargs["Tags"] = tags - - cloudwatch_commercial.put_metric_alarm( - AlarmName=alarm_name, - AlarmActions=[config.NOTIFICATIONS_SNS_TOPIC_ARN], + _create_cloudwatch_alarm( + alarm_name, + sns_notification_topic_arn, + tags, MetricName="HealthCheckStatus", Namespace="AWS/Route53", Statistic="Minimum", @@ -175,6 +246,17 @@ def _create_health_check_alarm(health_check_id, tags) -> str: "Value": health_check_id, } ], + ) + return alarm_name + + +def _create_cloudwatch_alarm(alarm_name, notification_sns_topic_arn, tags, **kwargs): + if tags: + kwargs["Tags"] = tags + + cloudwatch_commercial.put_metric_alarm( + AlarmName=alarm_name, + AlarmActions=[notification_sns_topic_arn], Period=60, EvaluationPeriods=1, DatapointsToAlarm=1, @@ -196,10 +278,20 @@ def _create_health_check_alarm(health_check_id, tags) -> str: }, ) - return alarm_name + +def _delete_cloudwatch_health_check_alarms( + existing_health_check_alarms, alarm_names_to_delete +): + _delete_alarms(alarm_names_to_delete) + existing_health_check_alarms = [ + health_check_alarm + for health_check_alarm in existing_health_check_alarms + if health_check_alarm["alarm_name"] not in alarm_names_to_delete + ] + return existing_health_check_alarms -def _delete_alarms(existing_health_check_alarms, alarm_names_to_delete): +def _delete_alarms(alarm_names_to_delete): try: cloudwatch_commercial.delete_alarms(AlarmNames=alarm_names_to_delete) except ClientError as e: @@ -213,13 +305,11 @@ def _delete_alarms(existing_health_check_alarms, alarm_names_to_delete): f"Got this error code deleting Cloudwatch alarms: {e.response['Error']}" ) raise e - existing_health_check_alarms = [ - health_check_alarm - for health_check_alarm in existing_health_check_alarms - if health_check_alarm["alarm_name"] not in alarm_names_to_delete - ] - return existing_health_check_alarms + + +def generate_ddos_alarm_name(service_instance_id): + return f"{config.AWS_RESOURCE_PREFIX}-{service_instance_id}-DDoSDetected" def _get_alarm_name(health_check_id): - return f"{config.CLOUDWATCH_ALARM_NAME_PREFIX}-{health_check_id}" + return f"{config.AWS_RESOURCE_PREFIX}-{health_check_id}" diff --git a/broker/tasks/sns.py b/broker/tasks/sns.py new file mode 100644 index 00000000..40cbd5a7 --- /dev/null +++ b/broker/tasks/sns.py @@ -0,0 +1,71 @@ +import logging + +from botocore.exceptions import ClientError +from sqlalchemy.orm.attributes import flag_modified + +from broker.aws import sns_commercial + +from broker.extensions import config, db + +from broker.models import Operation +from broker.tasks import huey + +logger = logging.getLogger(__name__) + + +@huey.retriable_task +def create_notification_topic(operation_id: int, **kwargs): + operation = db.session.get(Operation, operation_id) + service_instance = operation.service_instance + + operation.step_description = "Creating SNS notification topic" + flag_modified(operation, "step_description") + db.session.add(operation) + db.session.commit() + + kwargs = {} + if service_instance.tags: + kwargs["Tags"] = service_instance.tags + + response = sns_commercial.create_topic( + Name=f"{config.AWS_RESOURCE_PREFIX}-{service_instance.id}-notifications", + **kwargs, + ) + service_instance.sns_notification_topic_arn = response["TopicArn"] + db.session.add(service_instance) + db.session.commit() + + +@huey.retriable_task +def delete_notification_topic(operation_id: int, **kwargs): + operation = db.session.get(Operation, operation_id) + service_instance = operation.service_instance + + operation.step_description = "Deleting SNS notification topic" + flag_modified(operation, "step_description") + db.session.add(operation) + db.session.commit() + + if not service_instance.sns_notification_topic_arn: + logger.info(f"No SNS topic to delete for instance {service_instance.id}") + return + + try: + sns_commercial.delete_topic( + TopicArn=service_instance.sns_notification_topic_arn + ) + except ClientError as e: + if "NotFound" in e.response["Error"]["Code"]: + logger.info( + "SNS topic not found", + extra={"topic_arn": service_instance.sns_notification_topic_arn}, + ) + else: + logger.error( + f"Got this error code deleting SNS topic {service_instance.sns_notification_topic_arn}: {e.response['Error']}" + ) + raise e + + service_instance.sns_notification_topic_arn = None + db.session.add(service_instance) + db.session.commit() diff --git a/broker/tasks/waf.py b/broker/tasks/waf.py index aff1b3d6..895b7f49 100644 --- a/broker/tasks/waf.py +++ b/broker/tasks/waf.py @@ -105,7 +105,7 @@ def delete_web_acl(operation_id: str, **kwargs): def generate_web_acl_name(service_instance): - return f"{config.DEDICATED_WAF_NAME_PREFIX}-{service_instance.id}-dedicated-waf" + return f"{config.AWS_RESOURCE_PREFIX}-{service_instance.id}-dedicated-waf" def _delete_web_acl_with_retries(operation_id, service_instance): diff --git a/ci/pipeline.yml b/ci/pipeline.yml index bcdd1daf..d2517570 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -102,7 +102,6 @@ cf-manifest-env-dev: &cf-manifest-env-dev UAA_BASE_URL: ((dev-uaa-base-url)) UAA_CLIENT_ID: ((dev-uaa-client-id)) UAA_CLIENT_SECRET: ((dev-uaa-client-secret)) - NOTIFICATIONS_SNS_TOPIC_ARN: ((dev-sns-notifications-topic-arn)) cf-manifest-env-staging: &cf-manifest-env-staging environment_variables: @@ -136,7 +135,6 @@ cf-manifest-env-staging: &cf-manifest-env-staging UAA_BASE_URL: ((staging-uaa-base-url)) UAA_CLIENT_ID: ((staging-uaa-client-id)) UAA_CLIENT_SECRET: ((staging-uaa-client-secret)) - NOTIFICATIONS_SNS_TOPIC_ARN: ((staging-sns-notifications-topic-arn)) cf-manifest-env-production: &cf-manifest-env-production environment_variables: @@ -170,7 +168,6 @@ cf-manifest-env-production: &cf-manifest-env-production UAA_BASE_URL: ((prod-uaa-base-url)) UAA_CLIENT_ID: ((production-uaa-client-id)) UAA_CLIENT_SECRET: ((production-uaa-client-secret)) - NOTIFICATIONS_SNS_TOPIC_ARN: ((production-sns-notifications-topic-arn)) acceptance-tests-staging-params: &acceptance-tests-staging-params CF_API_URL: ((staging-cf-api-url)) @@ -260,15 +257,6 @@ jobs: channel: ((slack-failure-channel)) username: ((slack-username)) icon_url: ((slack-icon-url)) - on_success: - put: slack - params: &slack-success-params - text: | - :white_check_mark: Testing passed for external-domain-broker - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - channel: ((slack-success-channel)) - username: ((slack-username)) - icon_url: ((slack-icon-url)) - name: dev plan: @@ -330,13 +318,6 @@ jobs: text: | :x: FAILED to deploy external-domain-broker on development <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - on_success: - put: slack - params: - <<: *slack-success-params - text: | - :white_check_mark: Successfully deployed external-domain-broker on development - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - name: staging serial_groups: [staging] @@ -405,13 +386,6 @@ jobs: text: | :x: FAILED to deploy external-domain-broker on staging <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - on_success: - put: slack - params: - <<: *slack-success-params - text: | - :white_check_mark: Successfully deployed external-domain-broker on staging - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - name: staging-acceptance-tests @@ -474,6 +448,7 @@ jobs: TEST_DOMAIN_0: ((staging-test-domain-0)) HOSTED_ZONE_ID_1: ((staging-test-hosted-zone-id-1)) TEST_DOMAIN_1: ((staging-test-domain-1)) + ALARM_NOTIFICATION_EMAIL: ((test-alarm-notification-email)) run: path: /app/acceptance/run.sh on_failure: @@ -483,13 +458,6 @@ jobs: text: | :x: Acceptance tests for external-domain-broker FAILED in staging <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - on_success: - put: slack - params: - <<: *slack-success-params - text: | - :white_check_mark: Acceptance tests for external-domain-broker PASSED in staging - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - name: production serial_groups: [production] @@ -558,13 +526,6 @@ jobs: text: | :x: FAILED to deploy external-domain-broker on production <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - on_success: - put: slack - params: - <<: *slack-success-params - text: | - :white_check_mark: Successfully deployed external-domain-broker on production - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - name: production-acceptance-tests serial_groups: [production] @@ -626,6 +587,7 @@ jobs: params: <<: *acceptance-tests-production-params PLAN_NAME: "domain-with-cdn-dedicated-waf" + ALARM_NOTIFICATION_EMAIL: ((test-alarm-notification-email)) run: path: /app/acceptance/run.sh on_failure: @@ -635,13 +597,6 @@ jobs: text: | :x: Acceptance tests for external-domain-broker FAILED in production <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - on_success: - put: slack - params: - <<: *slack-success-params - text: | - :white_check_mark: Acceptance tests for external-domain-broker PASSED in production - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - name: check-duplicate-certs-prod plan: @@ -674,13 +629,6 @@ jobs: text: | :x: Failed to check for duplicate external-domain-broker certificates in production <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - on_success: - put: slack - params: - <<: *slack-success-params - text: | - :white_check_mark: Successfully checked for duplicate external-domain-broker certificates in production - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - name: remove-duplicate-certs-prod plan: @@ -712,13 +660,6 @@ jobs: text: | :x: Failed to remove duplicate external-domain-broker certificates in production <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> - on_success: - put: slack - params: - <<: *slack-success-params - text: | - :white_check_mark: Successfully removed duplicate external-domain-broker certificates in production - <$ATC_EXTERNAL_URL/teams/$BUILD_TEAM_NAME/pipelines/$BUILD_PIPELINE_NAME/jobs/$BUILD_JOB_NAME/builds/$BUILD_NAME|View build details> ############################ # RESOURCES diff --git a/migrations/versions/5afacdbeec29_add_sns_notification_topic_arn.py b/migrations/versions/5afacdbeec29_add_sns_notification_topic_arn.py new file mode 100644 index 00000000..9ee72856 --- /dev/null +++ b/migrations/versions/5afacdbeec29_add_sns_notification_topic_arn.py @@ -0,0 +1,35 @@ +"""add_sns_notification_topic_arn + +Revision ID: 5afacdbeec29 +Revises: 9919681623cc +Create Date: 2024-09-18 20:35:48.398374 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "5afacdbeec29" +down_revision = "9919681623cc" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("service_instance", schema=None) as batch_op: + batch_op.add_column( + sa.Column("sns_notification_topic_arn", sa.String(), nullable=True) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("service_instance", schema=None) as batch_op: + batch_op.drop_column("sns_notification_topic_arn") + + # ### end Alembic commands ### diff --git a/migrations/versions/9919681623cc_add_alarm_notification_email.py b/migrations/versions/9919681623cc_add_alarm_notification_email.py new file mode 100644 index 00000000..202fc23c --- /dev/null +++ b/migrations/versions/9919681623cc_add_alarm_notification_email.py @@ -0,0 +1,35 @@ +"""add_alarm_notification_email + +Revision ID: 9919681623cc +Revises: 4c57fe392f3c +Create Date: 2024-09-18 17:56:18.850280 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "9919681623cc" +down_revision = "4c57fe392f3c" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("service_instance", schema=None) as batch_op: + batch_op.add_column( + sa.Column("alarm_notification_email", sa.String(), nullable=True) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("service_instance", schema=None) as batch_op: + batch_op.drop_column("alarm_notification_email") + + # ### end Alembic commands ### diff --git a/migrations/versions/e56aab1c13c0_add_ddos_detected_cloudwatch_alarm_name.py b/migrations/versions/e56aab1c13c0_add_ddos_detected_cloudwatch_alarm_name.py new file mode 100644 index 00000000..45f69dff --- /dev/null +++ b/migrations/versions/e56aab1c13c0_add_ddos_detected_cloudwatch_alarm_name.py @@ -0,0 +1,35 @@ +"""add_ddos_detected_cloudwatch_alarm_name + +Revision ID: e56aab1c13c0 +Revises: 5afacdbeec29 +Create Date: 2024-09-18 21:17:32.095791 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "e56aab1c13c0" +down_revision = "5afacdbeec29" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("service_instance", schema=None) as batch_op: + batch_op.add_column( + sa.Column("ddos_detected_cloudwatch_alarm_name", sa.String(), nullable=True) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("service_instance", schema=None) as batch_op: + batch_op.drop_column("ddos_detected_cloudwatch_alarm_name") + + # ### end Alembic commands ### diff --git a/tests/conftest.py b/tests/conftest.py index d1d10465..bebcbf11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ from tests.lib.fake_wafv2 import wafv2 # noqa F401 from tests.lib.fake_shield import shield # noqa F401 from tests.lib.fake_cloudwatch import cloudwatch_commercial # noqa F401 +from tests.lib.fake_sns import sns_commercial # noqa F401 from tests.lib.simple_regex import simple_regex # noqa F401 from tests.lib.cdn.instances import ( unmigrated_cdn_service_instance_operation_id, diff --git a/tests/integration/cdn_dedicated_waf/provision.py b/tests/integration/cdn_dedicated_waf/provision.py index d68ef8bc..d058e4ca 100644 --- a/tests/integration/cdn_dedicated_waf/provision.py +++ b/tests/integration/cdn_dedicated_waf/provision.py @@ -1,11 +1,10 @@ -import pytest # noqa F401 import uuid from broker.extensions import config, db from broker.models import ( CDNDedicatedWAFServiceInstance, ) -from broker.tasks.cloudwatch import _get_alarm_name +from broker.tasks.cloudwatch import _get_alarm_name, generate_ddos_alarm_name def subtest_provision_create_web_acl(tasks, wafv2, service_instance_id="4321"): @@ -27,9 +26,7 @@ def subtest_provision_create_web_acl(tasks, wafv2, service_instance_id="4321"): CDNDedicatedWAFServiceInstance, service_instance_id ) assert service_instance.dedicated_waf_web_acl_id - web_acl_name = ( - f"{config.DEDICATED_WAF_NAME_PREFIX}-{service_instance.id}-dedicated-waf" - ) + web_acl_name = f"{config.AWS_RESOURCE_PREFIX}-{service_instance.id}-dedicated-waf" assert service_instance.dedicated_waf_web_acl_id == f"{web_acl_name}-id" assert service_instance.dedicated_waf_web_acl_name assert service_instance.dedicated_waf_web_acl_name == web_acl_name @@ -121,7 +118,9 @@ def subtest_provision_creates_health_check_alarms( health_check_id = health_check["health_check_id"] alarm_name = _get_alarm_name(health_check_id) - cloudwatch_commercial.expect_put_metric_alarm(health_check_id, alarm_name, tags) + cloudwatch_commercial.expect_put_metric_alarm( + health_check_id, alarm_name, service_instance + ) alarm_arn = f"{health_check_id} ARN" expected_health_check_alarms.append( { @@ -145,3 +144,52 @@ def subtest_provision_creates_health_check_alarms( ) cloudwatch_commercial.assert_no_pending_responses() + + +def subtest_provision_creates_sns_notification_topic( + tasks, + sns_commercial, + instance_model, + service_instance_id="4321", +): + db.session.expunge_all() + service_instance = db.session.get(instance_model, service_instance_id) + + sns_commercial.expect_create_topic(service_instance) + + tasks.run_queued_tasks_and_enqueue_dependents() + sns_commercial.assert_no_pending_responses() + + db.session.expunge_all() + service_instance = db.session.get(instance_model, service_instance_id) + + assert ( + service_instance.sns_notification_topic_arn + == f"{service_instance.id}-notifications-arn" + ) + + +def subtest_provision_creates_ddos_detected_alarm( + tasks, + cloudwatch_commercial, + instance_model, + service_instance_id="4321", +): + db.session.expunge_all() + service_instance = db.session.get(instance_model, service_instance_id) + + alarm_name = generate_ddos_alarm_name(service_instance_id) + cloudwatch_commercial.expect_put_ddos_detected_alarm( + alarm_name, service_instance, service_instance.sns_notification_topic_arn + ) + cloudwatch_commercial.expect_describe_alarms( + alarm_name, [{"AlarmArn": f"ddos-{service_instance.id}-arn"}] + ) + + tasks.run_queued_tasks_and_enqueue_dependents() + cloudwatch_commercial.assert_no_pending_responses() + + db.session.expunge_all() + service_instance = db.session.get(instance_model, service_instance_id) + + assert service_instance.ddos_detected_cloudwatch_alarm_name == alarm_name diff --git a/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_deprovisioning.py b/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_deprovisioning.py index bab0cae7..d9d5a5d8 100644 --- a/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_deprovisioning.py +++ b/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_deprovisioning.py @@ -61,6 +61,8 @@ def service_instance(protection_id): "alarm_name": _get_alarm_name("foo.com ID"), }, ], + ddos_detected_cloudwatch_alarm_name="fake-alarm-name", + sns_notification_topic_arn="fake-sns-arn", dedicated_waf_web_acl_id="1234-dedicated-waf-id", dedicated_waf_web_acl_name="1234-dedicated-waf", dedicated_waf_web_acl_arn="1234-dedicated-waf-arn", @@ -115,7 +117,6 @@ def service_instance(protection_id): db.session.add(current_cert) db.session.add(new_cert) db.session.commit() - db.session.expunge_all() return service_instance @@ -129,12 +130,16 @@ def test_deprovision_continues_when_resources_dont_exist( shield, wafv2, cloudwatch_commercial, + sns_commercial, ): instance_model = CDNDedicatedWAFServiceInstance subtest_deprovision_creates_deprovision_operation(instance_model, client) subtest_deprovision_removes_ALIAS_records_when_missing(tasks, route53) subtest_deprovision_removes_TXT_records_when_missing(tasks, route53) + subtest_deprovision_deletes_delete_ddos_alarm_when_missing( + instance_model, tasks, service_instance, cloudwatch_commercial + ) subtest_deprovision_deletes_health_check_alarms_when_missing( instance_model, tasks, service_instance, cloudwatch_commercial ) @@ -144,6 +149,9 @@ def test_deprovision_continues_when_resources_dont_exist( subtest_deprovision_deletes_health_checks_when_missing( instance_model, tasks, service_instance, route53 ) + subtest_deprovision_delete_sns_notification_topic_when_missing( + instance_model, tasks, service_instance, sns_commercial + ) subtest_deprovision_disables_cloudfront_distribution_when_missing( instance_model, tasks, service_instance, cloudfront ) @@ -171,6 +179,7 @@ def test_deprovision_happy_path( shield, wafv2, cloudwatch_commercial, + sns_commercial, ): instance_model = CDNDedicatedWAFServiceInstance operation_id = subtest_deprovision_creates_deprovision_operation( @@ -185,6 +194,12 @@ def test_deprovision_happy_path( check_last_operation_description( client, "1234", operation_id, "Removing DNS TXT records" ) + subtest_deprovision_delete_ddos_alarm( + instance_model, tasks, service_instance, cloudwatch_commercial + ) + check_last_operation_description( + client, "1234", operation_id, "Deleting DDoS detection alarm" + ) subtest_deprovision_deletes_health_check_alarms( instance_model, tasks, service_instance, cloudwatch_commercial ) @@ -206,6 +221,12 @@ def test_deprovision_happy_path( check_last_operation_description( client, "1234", operation_id, "Deleting health checks" ) + subtest_deprovision_delete_sns_notification_topic( + instance_model, tasks, service_instance, sns_commercial + ) + check_last_operation_description( + client, "1234", operation_id, "Deleting SNS notification topic" + ) subtest_deprovision_disables_cloudfront_distribution( instance_model, tasks, service_instance, cloudfront ) @@ -428,3 +449,67 @@ def subtest_deprovision_deletes_health_check_alarms_when_missing( db.session.expunge_all() service_instance = db.session.get(instance_model, "1234") assert service_instance.cloudwatch_health_check_alarms == [] + + +def subtest_deprovision_delete_sns_notification_topic( + instance_model, + tasks, + service_instance, + sns_commercial, +): + sns_commercial.expect_delete_topic(service_instance.sns_notification_topic_arn) + + tasks.run_queued_tasks_and_enqueue_dependents() + sns_commercial.assert_no_pending_responses() + + service_instance = db.session.get(instance_model, "1234") + assert service_instance.sns_notification_topic_arn == None + + +def subtest_deprovision_delete_sns_notification_topic_when_missing( + instance_model, + tasks, + service_instance, + sns_commercial, +): + sns_commercial.expect_delete_topic_not_found( + service_instance.sns_notification_topic_arn + ) + + tasks.run_queued_tasks_and_enqueue_dependents() + sns_commercial.assert_no_pending_responses() + + service_instance = db.session.get(instance_model, "1234") + assert service_instance.sns_notification_topic_arn == None + + +def subtest_deprovision_delete_ddos_alarm( + instance_model, tasks, service_instance, cloudwatch_commercial +): + cloudwatch_commercial.expect_delete_alarms( + [service_instance.ddos_detected_cloudwatch_alarm_name] + ) + + tasks.run_queued_tasks_and_enqueue_dependents() + cloudwatch_commercial.assert_no_pending_responses() + + db.session.expunge_all() + + service_instance = db.session.get(instance_model, "1234") + assert service_instance.ddos_detected_cloudwatch_alarm_name == None + + +def subtest_deprovision_deletes_delete_ddos_alarm_when_missing( + instance_model, tasks, service_instance, cloudwatch_commercial +): + cloudwatch_commercial.expect_delete_alarms_not_found( + [service_instance.ddos_detected_cloudwatch_alarm_name] + ) + + tasks.run_queued_tasks_and_enqueue_dependents() + cloudwatch_commercial.assert_no_pending_responses() + + db.session.expunge_all() + + service_instance = db.session.get(instance_model, "1234") + assert service_instance.ddos_detected_cloudwatch_alarm_name == None diff --git a/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_provisioning.py b/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_provisioning.py index 60a7de9d..fd1f3d83 100644 --- a/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_provisioning.py +++ b/tests/integration/cdn_dedicated_waf/test_cdn_dedicated_waf_provisioning.py @@ -52,6 +52,8 @@ subtest_provision_creates_health_checks, subtest_provision_associate_health_check, subtest_provision_creates_health_check_alarms, + subtest_provision_creates_sns_notification_topic, + subtest_provision_creates_ddos_detected_alarm, ) from tests.integration.cdn_dedicated_waf.update import ( subtest_update_web_acl_does_not_update, @@ -85,6 +87,7 @@ def test_provision_happy_path( wafv2, shield, cloudwatch_commercial, + sns_commercial, organization_guid, space_guid, mocked_cf_api, @@ -150,6 +153,12 @@ def test_provision_happy_path( check_last_operation_description( client, "4321", operation_id, "Waiting for DNS changes" ) + subtest_provision_creates_sns_notification_topic( + tasks, sns_commercial, instance_model + ) + check_last_operation_description( + client, "4321", operation_id, "Creating SNS notification topic" + ) subtest_provision_creates_health_checks(tasks, route53, instance_model) check_last_operation_description( client, "4321", operation_id, "Creating new health checks" @@ -167,6 +176,12 @@ def test_provision_happy_path( operation_id, "Creating Cloudwatch alarms for Route53 health checks", ) + subtest_provision_creates_ddos_detected_alarm( + tasks, cloudwatch_commercial, instance_model + ) + check_last_operation_description( + client, "4321", operation_id, "Creating DDoS detection alarm" + ) subtest_provision_marks_operation_as_succeeded(tasks, instance_model) check_last_operation_description(client, "4321", operation_id, "Complete!") subtest_update_happy_path( diff --git a/tests/integration/cdn_dedicated_waf/update.py b/tests/integration/cdn_dedicated_waf/update.py index 94f0ed89..18e5ce5d 100644 --- a/tests/integration/cdn_dedicated_waf/update.py +++ b/tests/integration/cdn_dedicated_waf/update.py @@ -147,7 +147,6 @@ def subtest_updates_health_check_alarms( db.session.expunge_all() service_instance = db.session.get(instance_model, service_instance_id) - tags = service_instance.tags if service_instance.tags else [] expected_health_check_alarms = [] expect_delete_health_check_id = "example.com ID" expect_delete_alarm_names = [_get_alarm_name(expect_delete_health_check_id)] @@ -167,7 +166,7 @@ def subtest_updates_health_check_alarms( cloudwatch_commercial.expect_put_metric_alarm( expect_create_health_check_id, _get_alarm_name(expect_create_health_check_id), - tags, + service_instance, ) cloudwatch_commercial.expect_describe_alarms( _get_alarm_name(expect_create_health_check_id), diff --git a/tests/integration/plan_updates/test_cdn_update_to_cdn_dedicated_waf.py b/tests/integration/plan_updates/test_cdn_update_to_cdn_dedicated_waf.py index 310e5b08..e35f5f05 100644 --- a/tests/integration/plan_updates/test_cdn_update_to_cdn_dedicated_waf.py +++ b/tests/integration/plan_updates/test_cdn_update_to_cdn_dedicated_waf.py @@ -45,6 +45,8 @@ subtest_provision_creates_health_checks, subtest_provision_associate_health_check, subtest_provision_creates_health_check_alarms, + subtest_provision_creates_sns_notification_topic, + subtest_provision_creates_ddos_detected_alarm, ) @@ -58,6 +60,7 @@ def test_update_plan_only( dns, iam_commercial, cloudwatch_commercial, + sns_commercial, simple_regex, organization_guid, space_guid, @@ -130,11 +133,17 @@ def test_update_plan_only( subtest_update_same_domains_does_not_delete_server_certificate( tasks, instance_model ) + subtest_provision_creates_sns_notification_topic( + tasks, sns_commercial, instance_model + ) subtest_provision_creates_health_checks(tasks, route53, instance_model) subtest_provision_associate_health_check(tasks, shield, instance_model) subtest_provision_creates_health_check_alarms( tasks, cloudwatch_commercial, instance_model ) + subtest_provision_creates_ddos_detected_alarm( + tasks, cloudwatch_commercial, instance_model + ) subtest_update_marks_update_complete(tasks, instance_model) @@ -148,6 +157,7 @@ def test_update_plan_and_domains( dns, iam_commercial, cloudwatch_commercial, + sns_commercial, simple_regex, organization_guid, space_guid, @@ -188,6 +198,9 @@ def test_update_plan_and_domains( subtest_update_updates_ALIAS_records(tasks, route53, instance_model) subtest_waits_for_dns_changes(tasks, route53, instance_model) subtest_update_removes_certificate_from_iam(tasks, iam_commercial, instance_model) + subtest_provision_creates_sns_notification_topic( + tasks, sns_commercial, instance_model + ) subtest_provision_creates_health_checks( tasks, route53, instance_model, expected_domain_names=["bar.com", "foo.com"] ) @@ -203,11 +216,16 @@ def test_update_plan_and_domains( subtest_provision_creates_health_check_alarms( tasks, cloudwatch_commercial, instance_model ) + subtest_provision_creates_ddos_detected_alarm( + tasks, cloudwatch_commercial, instance_model + ) subtest_update_marks_update_complete(tasks, instance_model) def subtest_creates_update_plan_operation(client, service_instance_id): - client.update_cdn_to_cdn_dedicated_waf_instance(service_instance_id) + client.update_cdn_to_cdn_dedicated_waf_instance( + service_instance_id, params={"alarm_notification_email": "fake@local.host"} + ) db.session.expunge_all() assert client.response.status_code == 202, client.response.body @@ -221,6 +239,11 @@ def subtest_creates_update_plan_operation(client, service_instance_id): assert operation.action == "Update" assert operation.service_instance_id == service_instance_id + service_instance = db.session.get( + CDNDedicatedWAFServiceInstance, service_instance_id + ) + assert service_instance.alarm_notification_email == "fake@local.host" + return operation_id @@ -238,6 +261,7 @@ def subtest_update_creates_update_plan_and_domains_operation( "forward_cookies": "mycookie,myothercookie, anewcookie", "forward_headers": "x-my-header, x-your-header ", "insecure_origin": True, + "alarm_notification_email": "fake@local.host", }, ) db.session.expunge_all() @@ -258,6 +282,8 @@ def subtest_update_creates_update_plan_and_domains_operation( assert instance.domain_names == ["bar.com", "foo.com"] assert instance.cloudfront_origin_hostname == "new-origin.com" assert instance.cloudfront_origin_path == "/somewhere-else" + assert instance.alarm_notification_email == "fake@local.host" + return operation_id diff --git a/tests/integration/test_cdn_plan_updates.py b/tests/integration/test_cdn_plan_updates.py new file mode 100644 index 00000000..224d1d93 --- /dev/null +++ b/tests/integration/test_cdn_plan_updates.py @@ -0,0 +1,60 @@ +import pytest # noqa F401 + +from broker.models import CDNDedicatedWAFServiceInstance +from tests.lib import factories + + +@pytest.fixture +def service_instance(clean_db, service_instance_id): + service_instance = factories.CDNServiceInstanceFactory.create( + id=service_instance_id, + domain_names=["example.com", "foo.com"], + domain_internal="fake1234.cloudfront.net", + route53_alias_hosted_zone="Z2FDTNDATAQYW2", + cloudfront_distribution_id="FakeDistributionId", + cloudfront_origin_hostname="origin_hostname", + cloudfront_origin_path="origin_path", + origin_protocol_policy="https-only", + ) + current_cert = factories.CertificateFactory.create( + service_instance=service_instance, + private_key_pem="SOMEPRIVATEKEY", + iam_server_certificate_id="certificate_id", + iam_server_certificate_arn="certificate_arn", + iam_server_certificate_name="certificate_name", + leaf_pem="SOMECERTPEM", + fullchain_pem="FULLCHAINOFSOMECERTPEM", + id=1001, + ) + service_instance.current_certificate = current_cert + clean_db.session.add(service_instance) + clean_db.session.add(current_cert) + clean_db.session.commit() + return service_instance + + +def test_update_cdn_dedicated_waf_no_alarm_notification_email( + clean_db, client, service_instance +): + client.update_cdn_to_cdn_dedicated_waf_instance(service_instance.id) + clean_db.session.expunge_all() + + assert client.response.status_code == 400, client.response.body + + +def test_update_cdn_dedicated_waf_with_alarm_notification_email( + clean_db, client, service_instance +): + assert not hasattr(service_instance, "alarm_notification_email") + + client.update_cdn_to_cdn_dedicated_waf_instance( + service_instance.id, params={"alarm_notification_email": "foo@bar.com"} + ) + clean_db.session.expunge_all() + + assert client.response.status_code == 202, client.response.body + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, service_instance.id + ) + assert service_instance.alarm_notification_email == "foo@bar.com" diff --git a/tests/integration/test_cdn_provision_common_checks.py b/tests/integration/test_cdn_provision_common_checks.py index fd6ee864..33301569 100644 --- a/tests/integration/test_cdn_provision_common_checks.py +++ b/tests/integration/test_cdn_provision_common_checks.py @@ -1,4 +1,5 @@ import pytest # noqa F401 +from openbrokerapi import errors from broker.extensions import config, db @@ -8,18 +9,29 @@ ) +@pytest.fixture +def provision_params(): + return {"domains": "example.com", "alarm_notification_email": "foo@bar.com"} + + @pytest.mark.parametrize( "instance_model", [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_default_origin_and_path_if_none_provided( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") client.provision_instance( instance_model, "4321", - params={"domains": "example.com"}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -38,13 +50,19 @@ def test_provision_sets_default_origin_and_path_if_none_provided( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_default_cookie_policy_if_none_provided( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") client.provision_instance( instance_model, "4321", - params={"domains": "example.com"}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -58,13 +76,20 @@ def test_provision_sets_default_cookie_policy_if_none_provided( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_none_cookie_policy( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update({"forward_cookies": ""}) client.provision_instance( instance_model, "4321", - params={"domains": "example.com", "forward_cookies": ""}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -78,16 +103,20 @@ def test_provision_sets_none_cookie_policy( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_forward_cookie_policy_with_cookies( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update({"forward_cookies": "my_cookie , my_other_cookie"}) client.provision_instance( instance_model, "4321", - params={ - "domains": "example.com", - "forward_cookies": "my_cookie , my_other_cookie", - }, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -101,13 +130,20 @@ def test_provision_sets_forward_cookie_policy_with_cookies( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_forward_cookie_policy_with_star( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update({"forward_cookies": "*"}) client.provision_instance( instance_model, "4321", - params={"domains": "example.com", "forward_cookies": "*"}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -121,13 +157,19 @@ def test_provision_sets_forward_cookie_policy_with_star( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_forward_headers_to_host_when_none_specified( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") client.provision_instance( instance_model, "4321", - params={"domains": "example.com"}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -140,16 +182,20 @@ def test_provision_sets_forward_headers_to_host_when_none_specified( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_forward_headers_plus_host_when_some_specified( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update({"forward_headers": "x-my-header,x-your-header"}) client.provision_instance( instance_model, "4321", - params={ - "domains": "example.com", - "forward_headers": "x-my-header,x-your-header", - }, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -164,13 +210,20 @@ def test_provision_sets_forward_headers_plus_host_when_some_specified( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_does_not_set_host_header_when_using_custom_origin( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update({"origin": "my-origin.example.gov"}) client.provision_instance( instance_model, "4321", - params={"domains": "example.com", "origin": "my-origin.example.gov"}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -183,13 +236,19 @@ def test_provision_does_not_set_host_header_when_using_custom_origin( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_https_only_by_default( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") client.provision_instance( instance_model, "4321", - params={"domains": "example.com"}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -202,17 +261,25 @@ def test_provision_sets_https_only_by_default( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_http_when_set( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update( + { + "origin": "origin.gov", + "insecure_origin": True, + } + ) client.provision_instance( instance_model, "4321", - params={ - "domains": "example.com", - "origin": "origin.gov", - "insecure_origin": True, - }, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -225,13 +292,24 @@ def test_provision_sets_http_when_set( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_refuses_insecure_origin_for_default_origin( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update( + { + "insecure_origin": True, + } + ) client.provision_instance( instance_model, "4321", - params={"domains": "example.com", "insecure_origin": True}, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) @@ -245,18 +323,96 @@ def test_provision_refuses_insecure_origin_for_default_origin( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_provision_sets_custom_error_responses( - dns, client, organization_guid, space_guid, instance_model, mocked_cf_api + dns, + client, + organization_guid, + space_guid, + provision_params, + instance_model, + mocked_cf_api, ): dns.add_cname("_acme-challenge.example.com") + provision_params.update({"error_responses": {"404": "/errors/404.html"}}) client.provision_instance( instance_model, "4321", - params={ - "domains": "example.com", - "error_responses": {"404": "/errors/404.html"}, - }, + params=provision_params, organization_guid=organization_guid, space_guid=space_guid, ) instance = db.session.get(instance_model, "4321") assert instance.error_responses["404"] == "/errors/404.html" + + +@pytest.mark.parametrize( + "instance_model, alarm_notification_email_param, expected_alarm_notification_email", + [ + [CDNServiceInstance, "foo@bar.com", None], + [CDNDedicatedWAFServiceInstance, "foo@bar.com", "foo@bar.com"], + ], +) +def test_provision_sets_alarm_notification_email( + dns, + client, + organization_guid, + space_guid, + instance_model, + service_instance_id, + provision_params, + alarm_notification_email_param, + expected_alarm_notification_email, + mocked_cf_api, +): + dns.add_cname("_acme-challenge.example.com") + provision_params.update( + { + "alarm_notification_email": alarm_notification_email_param, + } + ) + client.provision_instance( + instance_model, + service_instance_id, + params=provision_params, + organization_guid=organization_guid, + space_guid=space_guid, + ) + instance = db.session.get(instance_model, service_instance_id) + alarm_notification_email = ( + instance.alarm_notification_email + if hasattr(instance, "alarm_notification_email") + else None + ) + assert alarm_notification_email == expected_alarm_notification_email + + +@pytest.mark.parametrize( + "instance_model, response_status_code", + [ + [CDNServiceInstance, 202], + [CDNDedicatedWAFServiceInstance, 400], + ], +) +def test_provision_no_alarm_notification_email( + dns, + client, + organization_guid, + space_guid, + instance_model, + provision_params, + service_instance_id, + response_status_code, + mocked_cf_api, +): + dns.add_cname("_acme-challenge.example.com") + + provision_params["alarm_notification_email"] = None + + client.provision_instance( + instance_model, + service_instance_id, + params=provision_params, + organization_guid=organization_guid, + space_guid=space_guid, + ) + + assert client.response.status_code == response_status_code diff --git a/tests/integration/test_cdn_update_common_checks.py b/tests/integration/test_cdn_update_common_checks.py index 5e77439c..95463d3a 100644 --- a/tests/integration/test_cdn_update_common_checks.py +++ b/tests/integration/test_cdn_update_common_checks.py @@ -11,6 +11,11 @@ from tests.lib import factories +@pytest.fixture +def params_with_alarm_notification_email(): + return {"alarm_notification_email": "fake@localhost"} + + @pytest.fixture() def service_instance_factory(instance_model): if instance_model == CDNDedicatedWAFServiceInstance: @@ -78,7 +83,6 @@ def service_instance(service_instance_factory): db.session.add(current_cert) db.session.add(new_cert) db.session.commit() - db.session.expunge_all() return service_instance @@ -86,10 +90,16 @@ def service_instance(service_instance_factory): "instance_model", [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) -def test_provision_sets_default_origin_if_provided_as_none( - instance_model, client, service_instance +def test_update_sets_default_origin_if_provided_as_none( + instance_model, + client, + params_with_alarm_notification_email, + service_instance, ): - client.update_instance(instance_model, "4321", params={"origin": None}) + params_with_alarm_notification_email.update({"origin": None}) + client.update_instance( + instance_model, "4321", params=params_with_alarm_notification_email + ) db.session.expunge_all() assert client.response.status_code == 202, client.response.body @@ -108,10 +118,13 @@ def test_provision_sets_default_origin_if_provided_as_none( "instance_model", [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) -def test_provision_sets_default_origin_path_if_provided_as_none( - instance_model, client, service_instance +def test_update_sets_default_origin_path_if_provided_as_none( + instance_model, client, params_with_alarm_notification_email, service_instance ): - client.update_instance(instance_model, "4321", params={"path": None}) + params_with_alarm_notification_email.update({"path": None}) + client.update_instance( + instance_model, "4321", params=params_with_alarm_notification_email + ) db.session.expunge_all() assert client.response.status_code == 202, client.response.body @@ -129,14 +142,17 @@ def test_provision_sets_default_origin_path_if_provided_as_none( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_update_sets_default_cookie_policy_if_provided_as_none( - instance_model, client, service_instance + instance_model, client, params_with_alarm_notification_email, service_instance ): service_instance.forward_cookie_policy = "whitelist" service_instance.forwarded_cookies = ["foo", "bar"] db.session.add(service_instance) db.session.commit() - client.update_instance(instance_model, "4321", params={"forward_cookies": None}) + params_with_alarm_notification_email.update({"forward_cookies": None}) + client.update_instance( + instance_model, "4321", params=params_with_alarm_notification_email + ) db.session.expunge_all() instance = db.session.get(instance_model, "4321") assert instance.forward_cookie_policy == "all" @@ -147,12 +163,17 @@ def test_update_sets_default_cookie_policy_if_provided_as_none( "instance_model", [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) -def test_update_sets_none_cookie_policy(instance_model, client, service_instance): +def test_update_sets_none_cookie_policy( + instance_model, client, params_with_alarm_notification_email, service_instance +): service_instance.forward_cookie_policy = "whitelist" service_instance.forwarded_cookies = ["foo", "bar"] db.session.add(service_instance) db.session.commit() - client.update_instance(instance_model, "4321", params={"forward_cookies": ""}) + params_with_alarm_notification_email.update({"forward_cookies": ""}) + client.update_instance( + instance_model, "4321", params=params_with_alarm_notification_email + ) db.session.expunge_all() instance = db.session.get(instance_model, "4321") assert instance.forward_cookie_policy == "none" @@ -181,17 +202,21 @@ def test_update_sets_forward_cookie_policy_with_cookies( [CDNServiceInstance, CDNDedicatedWAFServiceInstance], ) def test_update_sets_forward_cookie_policy_with_star( - instance_model, client, dns, service_instance + instance_model, client, dns, params_with_alarm_notification_email, service_instance ): service_instance.forward_cookie_policy = "whitelist" service_instance.forwarded_cookies = ["foo", "bar"] db.session.add(service_instance) db.session.commit() dns.add_cname("_acme-challenge.example.com") + + params_with_alarm_notification_email.update( + {"domains": "example.com", "forward_cookies": "*"} + ) client.update_instance( instance_model, "4321", - params={"domains": "example.com", "forward_cookies": "*"}, + params=params_with_alarm_notification_email, ) db.session.expunge_all() instance = db.session.get(instance_model, "4321") @@ -285,3 +310,63 @@ def test_update_refuses_insecure_origin_for_default_origin( desc = client.response.json.get("description") assert client.response.status_code == 400 assert "insecure_origin" in desc + + +@pytest.mark.parametrize( + "instance_model, alarm_notification_email_param, expected_alarm_notification_email", + [ + [CDNServiceInstance, "foo@bar.com", None], + [CDNDedicatedWAFServiceInstance, "foo@bar.com", "foo@bar.com"], + ], +) +def test_update_sets_alarm_notification_email( + dns, + client, + instance_model, + alarm_notification_email_param, + expected_alarm_notification_email, + service_instance, + mocked_cf_api, +): + dns.add_cname("_acme-challenge.example.com") + client.update_instance( + instance_model, + service_instance.id, + params={ + "domains": ["example.com"], + "alarm_notification_email": alarm_notification_email_param, + }, + ) + instance = db.session.get(instance_model, service_instance.id) + alarm_notification_email = ( + instance.alarm_notification_email + if hasattr(instance, "alarm_notification_email") + else None + ) + assert alarm_notification_email == expected_alarm_notification_email + + +@pytest.mark.parametrize( + "instance_model, expected_response_status", + [ + [CDNServiceInstance, 202], + [CDNDedicatedWAFServiceInstance, 400], + ], +) +def test_update_no_alarm_notification_email( + dns, + client, + instance_model, + expected_response_status, + service_instance, + mocked_cf_api, +): + dns.add_cname("_acme-challenge.example.com") + client.update_instance( + instance_model, + service_instance.id, + params={ + "domains": ["example.com"], + }, + ) + assert client.response.status_code == expected_response_status diff --git a/tests/integration/test_cloudwatch.py b/tests/integration/test_cloudwatch.py index b759e89a..5e15157b 100644 --- a/tests/integration/test_cloudwatch.py +++ b/tests/integration/test_cloudwatch.py @@ -1,12 +1,15 @@ import pytest -from botocore.exceptions import WaiterError +from botocore.exceptions import WaiterError, ClientError from broker.tasks.cloudwatch import ( create_health_check_alarms, update_health_check_alarms, delete_health_check_alarms, + create_ddos_detected_alarm, + delete_ddos_detected_alarm, _get_alarm_name, + generate_ddos_alarm_name, ) from broker.extensions import config from broker.models import Operation, CDNDedicatedWAFServiceInstance @@ -42,6 +45,7 @@ def service_instance( "health_check_id": "foo.com ID", }, ], + sns_notification_topic_arn=f"{service_instance_id}-notifications-arn", ) new_cert = factories.CertificateFactory.create( service_instance=service_instance, @@ -80,10 +84,10 @@ def test_create_health_check_alarms( expected_health_check_alarms = [] for health_check in service_instance.route53_health_checks: health_check_id = health_check["health_check_id"] - alarm_name = f"{config.CLOUDWATCH_ALARM_NAME_PREFIX}-{health_check_id}" + alarm_name = f"{config.AWS_RESOURCE_PREFIX}-{health_check_id}" cloudwatch_commercial.expect_put_metric_alarm( - health_check_id, alarm_name, service_instance.tags + health_check_id, alarm_name, service_instance ) cloudwatch_commercial.expect_describe_alarms( alarm_name, [{"AlarmArn": f"{health_check_id} ARN"}] @@ -132,16 +136,18 @@ def test_create_health_check_alarms_unmigrated_cdn_instance( {"domain_name": "foo.com", "health_check_id": "foo.com ID"}, ] service_instance.route53_health_checks = route53_health_checks + service_instance.sns_notification_topic_arn = "fake-arn" clean_db.session.add(service_instance) clean_db.session.commit() - clean_db.session.expunge_all() expected_health_check_alarms = [] for health_check in route53_health_checks: health_check_id = health_check["health_check_id"] - alarm_name = f"{config.CLOUDWATCH_ALARM_NAME_PREFIX}-{health_check_id}" + alarm_name = f"{config.AWS_RESOURCE_PREFIX}-{health_check_id}" - cloudwatch_commercial.expect_put_metric_alarm(health_check_id, alarm_name, None) + cloudwatch_commercial.expect_put_metric_alarm( + health_check_id, alarm_name, service_instance + ) cloudwatch_commercial.expect_describe_alarms( alarm_name, [{"AlarmArn": f"{health_check_id} ARN"}] ) @@ -178,7 +184,7 @@ def test_create_health_check_alarm_waits( expected_health_check_alarms = [] health_check_id = service_instance.route53_health_checks[0]["health_check_id"] - alarm_name = f"{config.CLOUDWATCH_ALARM_NAME_PREFIX}-{health_check_id}" + alarm_name = f"{config.AWS_RESOURCE_PREFIX}-{health_check_id}" expected_health_check_alarms.append( { "alarm_name": alarm_name, @@ -187,7 +193,7 @@ def test_create_health_check_alarm_waits( ) cloudwatch_commercial.expect_put_metric_alarm( - health_check_id, alarm_name, service_instance.tags + health_check_id, alarm_name, service_instance ) # waiting for alarm to exist cloudwatch_commercial.expect_describe_alarms(alarm_name, []) @@ -197,7 +203,7 @@ def test_create_health_check_alarm_waits( ) health_check_id = service_instance.route53_health_checks[1]["health_check_id"] - alarm_name = f"{config.CLOUDWATCH_ALARM_NAME_PREFIX}-{health_check_id}" + alarm_name = f"{config.AWS_RESOURCE_PREFIX}-{health_check_id}" expected_health_check_alarms.append( { "alarm_name": alarm_name, @@ -206,7 +212,7 @@ def test_create_health_check_alarm_waits( ) cloudwatch_commercial.expect_put_metric_alarm( - health_check_id, alarm_name, service_instance.tags + health_check_id, alarm_name, service_instance ) # waiting for alarm to exist cloudwatch_commercial.expect_describe_alarms( @@ -235,10 +241,10 @@ def test_create_health_check_alarm_error_if_alarm_not_found( cloudwatch_commercial, ): health_check_id = service_instance.route53_health_checks[0]["health_check_id"] - alarm_name = f"{config.CLOUDWATCH_ALARM_NAME_PREFIX}-{health_check_id}" + alarm_name = f"{config.AWS_RESOURCE_PREFIX}-{health_check_id}" cloudwatch_commercial.expect_put_metric_alarm( - health_check_id, alarm_name, service_instance.tags + health_check_id, alarm_name, service_instance ) # waiting for alarm to exist for i in list(range(config.AWS_POLL_MAX_ATTEMPTS)): @@ -251,6 +257,24 @@ def test_create_health_check_alarm_error_if_alarm_not_found( cloudwatch_commercial.assert_no_pending_responses() +def test_create_health_check_alarms_no_topic( + clean_db, + service_instance, + operation_id, + cloudwatch_commercial, +): + service_instance.sns_notification_topic_arn = None + + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + with pytest.raises(RuntimeError): + create_health_check_alarms.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + def test_update_health_check_alarms( clean_db, service_instance_id, @@ -258,7 +282,7 @@ def test_update_health_check_alarms( operation_id, cloudwatch_commercial, ): - tags = service_instance.tags + tags = service_instance expect_delete_health_check_id = "foo.com ID" expect_delete_alarm_names = [_get_alarm_name(expect_delete_health_check_id)] expect_create_health_check_id = "bar.com ID" @@ -298,7 +322,6 @@ def test_update_health_check_alarms( clean_db.session.add(service_instance) clean_db.session.commit() - clean_db.session.expunge_all() cloudwatch_commercial.expect_delete_alarms(expect_delete_alarm_names) cloudwatch_commercial.expect_put_metric_alarm( @@ -339,7 +362,7 @@ def test_update_health_check_alarms_idempotent( operation_id, cloudwatch_commercial, ): - tags = service_instance.tags + tags = service_instance expect_delete_health_check_id = "foo.com ID" expect_delete_alarm_names = [_get_alarm_name(expect_delete_health_check_id)] expect_create_health_check_id = "bar.com ID" @@ -379,7 +402,6 @@ def test_update_health_check_alarms_idempotent( clean_db.session.add(service_instance) clean_db.session.commit() - clean_db.session.expunge_all() cloudwatch_commercial.expect_delete_alarms(expect_delete_alarm_names) cloudwatch_commercial.expect_put_metric_alarm( @@ -422,7 +444,7 @@ def test_update_health_check_alarms_unmigrated_instance( Operation, unmigrated_cdn_dedicated_waf_service_instance_operation_id ) service_instance = operation.service_instance - tags = service_instance.tags + tags = service_instance expect_create_health_check_id = "bar.com ID" expected_health_check_alarms = [ @@ -448,10 +470,9 @@ def test_update_health_check_alarms_unmigrated_instance( "health_check_id": "bar.com ID", }, ] - + service_instance.sns_notification_topic_arn = "fake-arn" clean_db.session.add(service_instance) clean_db.session.commit() - clean_db.session.expunge_all() cloudwatch_commercial.expect_put_metric_alarm( "example.com ID", @@ -490,6 +511,24 @@ def test_update_health_check_alarms_unmigrated_instance( ) +def test_update_health_check_alarms_no_topic( + clean_db, + service_instance, + operation_id, + cloudwatch_commercial, +): + service_instance.sns_notification_topic_arn = None + + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + with pytest.raises(RuntimeError): + update_health_check_alarms.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + def test_delete_health_check_alarms( clean_db, service_instance_id, @@ -611,7 +650,7 @@ def test_delete_health_check_alarms_unexpected_error( expect_delete_alarm_names ) - with pytest.raises(Exception): + with pytest.raises(ClientError): delete_health_check_alarms.call_local(operation_id) @@ -671,3 +710,267 @@ def test_delete_health_check_alarms_unmigrated_instance( service_instance_id, ) assert service_instance.cloudwatch_health_check_alarms == None + + +def test_create_ddos_detection_alarm( + clean_db, + service_instance_id, + service_instance, + operation_id, + cloudwatch_commercial, +): + alarm_name = generate_ddos_alarm_name(service_instance_id) + + cloudwatch_commercial.expect_put_ddos_detected_alarm( + alarm_name, service_instance, f"{service_instance.id}-notifications-arn" + ) + cloudwatch_commercial.expect_describe_alarms( + alarm_name, [{"AlarmArn": f"ddos-{service_instance.id}-arn"}] + ) + + create_ddos_detected_alarm.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + operation = clean_db.session.get(Operation, operation_id) + assert operation.step_description == "Creating DDoS detection alarm" + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == alarm_name + + +def test_create_ddos_detection_alarm_with_tags( + clean_db, + service_instance_id, + service_instance, + operation_id, + cloudwatch_commercial, +): + tags = [{"Key": "foo", "Value": "bar"}] + service_instance.tags = tags + clean_db.session.add(service_instance) + clean_db.session.commit() + + alarm_name = generate_ddos_alarm_name(service_instance_id) + + cloudwatch_commercial.expect_put_ddos_detected_alarm( + alarm_name, service_instance, f"{service_instance.id}-notifications-arn" + ) + cloudwatch_commercial.expect_describe_alarms( + alarm_name, [{"AlarmArn": f"ddos-{service_instance.id}-arn"}] + ) + + create_ddos_detected_alarm.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == alarm_name + + +def test_create_ddos_detection_alarm_no_topic( + clean_db, + service_instance, + operation_id, + cloudwatch_commercial, +): + service_instance.sns_notification_topic_arn = None + + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + with pytest.raises(RuntimeError): + create_ddos_detected_alarm.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + +def test_create_ddos_detection_alarm_unmigrated_instance( + clean_db, + service_instance_id, + unmigrated_cdn_service_instance_operation_id, + cloudwatch_commercial, +): + operation = clean_db.session.get( + Operation, unmigrated_cdn_service_instance_operation_id + ) + service_instance = operation.service_instance + + assert service_instance.ddos_detected_cloudwatch_alarm_name == None + + sns_notification_topic_arn = f"{service_instance.id}-notifications-arn" + service_instance.sns_notification_topic_arn = sns_notification_topic_arn + clean_db.session.add(service_instance) + clean_db.session.commit() + + alarm_name = generate_ddos_alarm_name(service_instance_id) + + cloudwatch_commercial.expect_put_ddos_detected_alarm( + alarm_name, service_instance, sns_notification_topic_arn + ) + cloudwatch_commercial.expect_describe_alarms( + alarm_name, [{"AlarmArn": f"ddos-{service_instance.id}-arn"}] + ) + + create_ddos_detected_alarm.call_local(unmigrated_cdn_service_instance_operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == alarm_name + + +def test_delete_ddos_detection_alarm( + clean_db, + service_instance_id, + service_instance, + operation_id, + cloudwatch_commercial, +): + alarm_name = generate_ddos_alarm_name(service_instance_id) + service_instance.ddos_detected_cloudwatch_alarm_name = alarm_name + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + cloudwatch_commercial.expect_delete_alarms([alarm_name]) + + delete_ddos_detected_alarm.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + operation = clean_db.session.get(Operation, operation_id) + assert operation.step_description == "Deleting DDoS detection alarm" + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == None + + +def test_delete_ddos_detection_alarm_no_alarm_value( + clean_db, + service_instance_id, + service_instance, + operation_id, + cloudwatch_commercial, +): + assert service_instance.ddos_detected_cloudwatch_alarm_name == None + + delete_ddos_detected_alarm.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == None + + +def test_delete_ddos_detection_alarm_not_found( + clean_db, + service_instance_id, + service_instance, + operation_id, + cloudwatch_commercial, +): + alarm_name = generate_ddos_alarm_name(service_instance_id) + service_instance.ddos_detected_cloudwatch_alarm_name = alarm_name + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + cloudwatch_commercial.expect_delete_alarms_not_found([alarm_name]) + + delete_ddos_detected_alarm.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == None + + +def test_delete_ddos_detection_alarm_unexpected_error( + clean_db, + service_instance_id, + service_instance, + operation_id, + cloudwatch_commercial, +): + alarm_name = generate_ddos_alarm_name(service_instance) + service_instance.ddos_detected_cloudwatch_alarm_name = alarm_name + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + cloudwatch_commercial.expect_delete_alarms_unexpected_error([alarm_name]) + + with pytest.raises(ClientError): + delete_ddos_detected_alarm.call_local(operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == alarm_name + + +def test_delete_ddos_detection_alarm_unmigrated_instance( + clean_db, + service_instance_id, + unmigrated_cdn_service_instance_operation_id, + cloudwatch_commercial, +): + alarm_name = generate_ddos_alarm_name(service_instance_id) + + operation = clean_db.session.get( + Operation, unmigrated_cdn_service_instance_operation_id + ) + service_instance = operation.service_instance + service_instance.ddos_detected_cloudwatch_alarm_name = alarm_name + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + cloudwatch_commercial.expect_delete_alarms_not_found([alarm_name]) + + delete_ddos_detected_alarm.call_local(unmigrated_cdn_service_instance_operation_id) + + cloudwatch_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.ddos_detected_cloudwatch_alarm_name == None diff --git a/tests/integration/test_provision_common_checks.py b/tests/integration/test_provision_common_checks.py index bbc105e5..1fcb66fc 100644 --- a/tests/integration/test_provision_common_checks.py +++ b/tests/integration/test_provision_common_checks.py @@ -1,5 +1,4 @@ -from datetime import date, datetime -import time +from datetime import datetime import pytest @@ -121,7 +120,10 @@ def test_doesnt_refuse_to_provision_with_duplicate_domains_when_not_configured_t client.provision_instance( instance_model, "4321", - params={"domains": "example.com, foo.com"}, + params={ + "domains": "example.com, foo.com", + "alarm_notification_email": "fake@localhost", + }, organization_guid=organization_guid, space_guid=space_guid, ) @@ -157,7 +159,10 @@ def test_duplicate_domain_check_ignores_deactivated( client.provision_instance( instance_model, "4321", - params={"domains": "foo.com"}, + params={ + "domains": "foo.com", + "alarm_notification_email": "fake@localhost", + }, organization_guid=organization_guid, space_guid=space_guid, ) diff --git a/tests/integration/test_sns.py b/tests/integration/test_sns.py new file mode 100644 index 00000000..d6a519d7 --- /dev/null +++ b/tests/integration/test_sns.py @@ -0,0 +1,256 @@ +import pytest + +from broker.tasks.sns import create_notification_topic, delete_notification_topic +from broker.models import Operation, CDNDedicatedWAFServiceInstance + +from tests.lib import factories + + +@pytest.fixture +def service_instance( + clean_db, + operation_id, + service_instance_id, + cloudfront_distribution_arn, +): + service_instance = factories.CDNDedicatedWAFServiceInstanceFactory.create( + id=service_instance_id, + domain_names=["example.com", "foo.com"], + domain_internal="fake1234.cloudfront.net", + route53_alias_hosted_zone="Z2FDTNDATAQYW2", + cloudfront_distribution_id="FakeDistributionId", + cloudfront_distribution_arn=cloudfront_distribution_arn, + cloudfront_origin_hostname="origin_hostname", + cloudfront_origin_path="origin_path", + origin_protocol_policy="https-only", + forwarded_headers=["HOST"], + route53_health_checks=[ + { + "domain_name": "example.com", + "health_check_id": "example.com ID", + }, + { + "domain_name": "foo.com", + "health_check_id": "foo.com ID", + }, + ], + ) + new_cert = factories.CertificateFactory.create( + service_instance=service_instance, + private_key_pem="SOMEPRIVATEKEY", + iam_server_certificate_id="certificate_id", + leaf_pem="SOMECERTPEM", + fullchain_pem="FULLCHAINOFSOMECERTPEM", + id=1002, + ) + current_cert = factories.CertificateFactory.create( + service_instance=service_instance, + private_key_pem="SOMEPRIVATEKEY", + iam_server_certificate_id="certificate_id", + id=1001, + ) + service_instance.current_certificate = current_cert + service_instance.new_certificate = new_cert + clean_db.session.add(service_instance) + clean_db.session.add(current_cert) + clean_db.session.add(new_cert) + clean_db.session.commit() + factories.OperationFactory.create( + id=operation_id, service_instance=service_instance + ) + return service_instance + + +def test_create_sns_notification_topic( + clean_db, + service_instance_id, + service_instance, + operation_id, + sns_commercial, +): + sns_commercial.expect_create_topic(service_instance) + + create_notification_topic.call_local(operation_id) + + sns_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + operation = clean_db.session.get(Operation, operation_id) + assert operation.step_description == "Creating SNS notification topic" + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert ( + service_instance.sns_notification_topic_arn + == f"{service_instance.id}-notifications-arn" + ) + + +def test_create_sns_notification_topic_with_tags( + clean_db, + service_instance_id, + service_instance, + operation_id, + sns_commercial, +): + tags = [{"Key": "foo", "Value": "bar"}] + service_instance.tags = tags + clean_db.session.add(service_instance) + clean_db.session.commit() + + sns_commercial.expect_create_topic(service_instance) + + create_notification_topic.call_local(operation_id) + + sns_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert ( + service_instance.sns_notification_topic_arn + == f"{service_instance.id}-notifications-arn" + ) + + +def test_create_sns_notification_topic_unmigrated_instance( + clean_db, + service_instance_id, + unmigrated_cdn_service_instance_operation_id, + sns_commercial, +): + operation = clean_db.session.get( + Operation, unmigrated_cdn_service_instance_operation_id + ) + service_instance = operation.service_instance + + assert service_instance.sns_notification_topic_arn == None + + sns_commercial.expect_create_topic(service_instance) + + create_notification_topic.call_local(unmigrated_cdn_service_instance_operation_id) + + sns_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert ( + service_instance.sns_notification_topic_arn + == f"{service_instance.id}-notifications-arn" + ) + + +def test_delete_sns_notification_topic( + clean_db, + service_instance_id, + service_instance, + operation_id, + sns_commercial, +): + service_instance.sns_notification_topic_arn = "fake-arn" + clean_db.session.add(service_instance) + clean_db.session.commit() + + sns_commercial.expect_delete_topic(service_instance.sns_notification_topic_arn) + + clean_db.session.expunge_all() + + delete_notification_topic.call_local(operation_id) + + sns_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + operation = clean_db.session.get(Operation, operation_id) + assert operation.step_description == "Deleting SNS notification topic" + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.sns_notification_topic_arn == None + + +def test_delete_sns_notification_topic_no_value( + clean_db, + service_instance, + operation_id, + sns_commercial, +): + service_instance.sns_notification_topic_arn = None + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + delete_notification_topic.call_local(operation_id) + + sns_commercial.assert_no_pending_responses() + + +def test_delete_sns_notification_topic_not_found( + clean_db, + service_instance_id, + service_instance, + operation_id, + sns_commercial, +): + service_instance.sns_notification_topic_arn = "fake-arn" + clean_db.session.add(service_instance) + clean_db.session.commit() + + sns_commercial.expect_delete_topic_not_found( + service_instance.sns_notification_topic_arn + ) + + clean_db.session.expunge_all() + + delete_notification_topic.call_local(operation_id) + + sns_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.sns_notification_topic_arn == None + + +def test_delete_sns_notification_topic_unmigrated_instance( + clean_db, + service_instance_id, + unmigrated_cdn_service_instance_operation_id, + sns_commercial, +): + operation = clean_db.session.get( + Operation, unmigrated_cdn_service_instance_operation_id + ) + service_instance = operation.service_instance + + service_instance.sns_notification_topic_arn = "fake-arn" + clean_db.session.add(service_instance) + clean_db.session.commit() + clean_db.session.expunge_all() + + sns_commercial.expect_delete_topic("fake-arn") + + delete_notification_topic.call_local(unmigrated_cdn_service_instance_operation_id) + + sns_commercial.assert_no_pending_responses() + + clean_db.session.expunge_all() + + service_instance = clean_db.session.get( + CDNDedicatedWAFServiceInstance, + service_instance_id, + ) + assert service_instance.sns_notification_topic_arn == None diff --git a/tests/integration/test_update_common_checks.py b/tests/integration/test_update_common_checks.py index cf9d18f1..e9530a5e 100644 --- a/tests/integration/test_update_common_checks.py +++ b/tests/integration/test_update_common_checks.py @@ -65,6 +65,9 @@ def alb_service_instance(service_instance_factory): def cdn_service_instance(service_instance_factory): + kwargs = {} + if service_instance_factory == factories.CDNDedicatedWAFServiceInstanceFactory: + kwargs["alarm_notification_email"] = "fake@localhost" service_instance = service_instance_factory.create( id="4321", domain_names=["example.com", "foo.com"], @@ -74,6 +77,7 @@ def cdn_service_instance(service_instance_factory): cloudfront_origin_hostname="origin_hostname", cloudfront_origin_path="origin_path", origin_protocol_policy="https-only", + **kwargs, ) new_cert = factories.CertificateFactory.create( service_instance=service_instance, @@ -210,7 +214,9 @@ def test_duplicate_domain_check_ignores_self( dns.add_cname("_acme-challenge.foo.com") client.update_instance( - instance_model, "4321", params={"domains": "example.com, foo.com"} + instance_model, + "4321", + params={"domains": "example.com, foo.com"}, ) assert client.response.status_code == expected_status_code, client.response.body diff --git a/tests/lib/cdn/instances.py b/tests/lib/cdn/instances.py index 00098f03..7ee510fa 100644 --- a/tests/lib/cdn/instances.py +++ b/tests/lib/cdn/instances.py @@ -33,7 +33,9 @@ def unmigrated_cdn_service_instance_operation_id( ) clean_db.session.execute(create_cdn_instance_statement) - client.update_cdn_to_cdn_dedicated_waf_instance(service_instance_id) + client.update_cdn_to_cdn_dedicated_waf_instance( + service_instance_id, params={"alarm_notification_email": "fake@local.host"} + ) operation_id = client.response.json["operation"] return operation_id @@ -63,6 +65,8 @@ def unmigrated_cdn_dedicated_waf_service_instance_operation_id( ) clean_db.session.execute(create_cdn_instance_statement) - client.update_cdn_to_cdn_dedicated_waf_instance(service_instance_id) + client.update_cdn_to_cdn_dedicated_waf_instance( + service_instance_id, params={"alarm_notification_email": "fake@local.host"} + ) operation_id = client.response.json["operation"] return operation_id diff --git a/tests/lib/cdn/provision.py b/tests/lib/cdn/provision.py index 48878da6..c558ae1a 100644 --- a/tests/lib/cdn/provision.py +++ b/tests/lib/cdn/provision.py @@ -105,23 +105,32 @@ def subtest_provision_creates_provision_operation( dns.add_cname("_acme-challenge.example.com") dns.add_cname("_acme-challenge.foo.com") + params = { + "domains": "example.com, Foo.com", + "origin": "origin.com", + "path": "/somewhere", + "forward_cookies": "mycookie,myothercookie", + "forward_headers": "x-my-header, x-your-header ", + "insecure_origin": True, + "error_responses": { + "404": "/errors/404.html", + "405": "/errors/405.html", + }, + } + + if instance_model == CDNServiceInstance: + service_plan_name = "domain-with-cdn" + elif instance_model == CDNDedicatedWAFServiceInstance: + service_plan_name = "domain-with-cdn-dedicated-waf" + params.update({"alarm_notification_email": "fake@local.host"}) + expected_alarm_notification_email = "fake@local.host" + provision_instance_with_mocks( client, instance_model, organization_guid, space_guid, - params={ - "domains": "example.com, Foo.com", - "origin": "origin.com", - "path": "/somewhere", - "forward_cookies": "mycookie,myothercookie", - "forward_headers": "x-my-header, x-your-header ", - "insecure_origin": True, - "error_responses": { - "404": "/errors/404.html", - "405": "/errors/405.html", - }, - }, + params=params, ) db.session.expunge_all() @@ -142,11 +151,9 @@ def subtest_provision_creates_provision_operation( assert instance.domain_names == ["example.com", "foo.com"] assert instance.cloudfront_origin_hostname == "origin.com" assert instance.cloudfront_origin_path == "/somewhere" + if hasattr(instance, "alarm_notification_email"): + assert instance.alarm_notification_email == expected_alarm_notification_email - if instance_model == CDNServiceInstance: - service_plan_name = "domain-with-cdn" - elif instance_model == CDNDedicatedWAFServiceInstance: - service_plan_name = "domain-with-cdn-dedicated-waf" assert sort_instance_tags(instance.tags) == sort_instance_tags( [ {"Key": "client", "Value": "Cloud Foundry"}, diff --git a/tests/lib/fake_cloudwatch.py b/tests/lib/fake_cloudwatch.py index 831bbf83..4a90f727 100644 --- a/tests/lib/fake_cloudwatch.py +++ b/tests/lib/fake_cloudwatch.py @@ -7,10 +7,12 @@ class FakeCloudwatch(FakeAWS): - def expect_put_metric_alarm(self, health_check_id: str, alarm_name: str, tags): + def expect_put_metric_alarm( + self, health_check_id: str, alarm_name: str, service_instance + ): request = { "AlarmName": alarm_name, - "AlarmActions": [config.NOTIFICATIONS_SNS_TOPIC_ARN], + "AlarmActions": [service_instance.sns_notification_topic_arn], "MetricName": "HealthCheckStatus", "Namespace": "AWS/Route53", "Statistic": "Minimum", @@ -26,8 +28,37 @@ def expect_put_metric_alarm(self, health_check_id: str, alarm_name: str, tags): "Threshold": 1, "ComparisonOperator": "LessThanThreshold", } - if tags: - request["Tags"] = tags + if service_instance.tags: + request["Tags"] = service_instance.tags + self.stubber.add_response( + "put_metric_alarm", + {}, + request, + ) + + def expect_put_ddos_detected_alarm( + self, alarm_name, service_instance, notification_topic_arn + ): + request = { + "AlarmName": alarm_name, + "AlarmActions": [notification_topic_arn], + "MetricName": "DDoSDetected", + "Namespace": "AWS/DDoSProtection", + "Statistic": "Minimum", + "Dimensions": [ + { + "Name": "ResourceArn", + "Value": service_instance.cloudfront_distribution_arn, + } + ], + "Period": 60, + "EvaluationPeriods": 1, + "DatapointsToAlarm": 1, + "Threshold": 1, + "ComparisonOperator": "LessThanThreshold", + } + if service_instance.tags: + request["Tags"] = service_instance.tags self.stubber.add_response( "put_metric_alarm", {}, diff --git a/tests/lib/fake_sns.py b/tests/lib/fake_sns.py new file mode 100644 index 00000000..d7549dac --- /dev/null +++ b/tests/lib/fake_sns.py @@ -0,0 +1,44 @@ +import pytest + + +from broker.aws import sns_commercial as real_sns_commercial +from broker.extensions import config + +from tests.lib.fake_aws import FakeAWS + + +class FakeSNS(FakeAWS): + def expect_create_topic(self, service_instance): + topic_name = f"{config.AWS_RESOURCE_PREFIX}-{service_instance.id}-notifications" + request = { + "Name": topic_name, + } + if service_instance.tags: + request["Tags"] = service_instance.tags + self.stubber.add_response( + "create_topic", + {"TopicArn": f"{service_instance.id}-notifications-arn"}, + request, + ) + + def expect_delete_topic(self, topic_arn): + self.stubber.add_response( + "delete_topic", + {}, + {"TopicArn": topic_arn}, + ) + + def expect_delete_topic_not_found(self, topic_arn): + self.stubber.add_client_error( + "delete_topic", + service_error_code="NotFoundException", + service_message="Not found", + http_status_code=404, + expected_params={"TopicArn": topic_arn}, + ) + + +@pytest.fixture(autouse=True) +def sns_commercial(): + with FakeSNS.stubbing(real_sns_commercial) as sns_stubber: + yield sns_stubber diff --git a/tests/lib/fake_wafv2.py b/tests/lib/fake_wafv2.py index 783b2a4c..b8d12bb0 100644 --- a/tests/lib/fake_wafv2.py +++ b/tests/lib/fake_wafv2.py @@ -9,7 +9,7 @@ class FakeWAFV2(FakeAWS): def expect_create_web_acl(self, id: str, rule_group_arn: str, tags: list[Tag]): method = "create_web_acl" - waf_name = f"{config.DEDICATED_WAF_NAME_PREFIX}-{id}-dedicated-waf" + waf_name = f"{config.AWS_RESOURCE_PREFIX}-{id}-dedicated-waf" request = { "Name": waf_name, diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index a116d8d8..79790ee7 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -121,7 +121,6 @@ def mocked_env( monkeypatch.setenv("UAA_BASE_URL", "mock://uaa") monkeypatch.setenv("UAA_CLIENT_ID", uaa_client_id) monkeypatch.setenv("UAA_CLIENT_SECRET", uaa_client_secret) - monkeypatch.setenv("NOTIFICATIONS_SNS_TOPIC_ARN", "fake-sns-topic-arn") @pytest.mark.parametrize("env", ["production", "staging", "development"])