diff --git a/CHANGELOG.md b/CHANGELOG.md index fc45343..ca32432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.3] - 2023-10-12 + +### Changed + +- Upgrade aws-cdk to 2.88.0 +- Upgrade deprecated methods in App-registry +- Address or Fix all SonarQube issues + ## [1.4.2] - 2023-06-22 ### Changed diff --git a/NOTICE.txt b/NOTICE.txt index 2d4d383..1865502 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -37,5 +37,13 @@ requests-mock under the Apache License Version 2.0 rich under the Massachusetts Institute of Technology (MIT) license tenacity under the Apache License Version 2.0 quartz-scheduler under the Apache License Version 2.0 +parsedatetime under the Apache License Version 2.0 +urllib3 under the Massachusetts Institute of Technology (MIT) license +setuptools under the Massachusetts Institute of Technology (MIT) license +pipenv under the Massachusetts Institute of Technology (MIT) license +virtualenv under the Massachusetts Institute of Technology (MIT) license +tox under the Massachusetts Institute of Technology (MIT) license +tox-pyenv under the Apache License Version 2.0 +poetry under the Massachusetts Institute of Technology (MIT) license The Apache License Version Version 2.0 is included in LICENSE.txt. \ No newline at end of file diff --git a/README.md b/README.md index b75a38e..8869564 100644 --- a/README.md +++ b/README.md @@ -609,7 +609,7 @@ The following procedures assumes that all the OS-level configuration has been co - [AWS Command Line Interface](https://aws.amazon.com/cli/) - [Python](https://www.python.org/) 3.9 or newer - [Node.js](https://nodejs.org/en/) 16.x or newer -- [AWS CDK](https://aws.amazon.com/cdk/) 2.75.0 or newer +- [AWS CDK](https://aws.amazon.com/cdk/) 2.88.0 or newer - [Amazon Corretto OpenJDK](https://docs.aws.amazon.com/corretto/) 17.0.4.1 > **Please ensure you test the templates before updating any production deployments.** @@ -707,7 +707,7 @@ After running the command, you can deploy the template: ## Collection of operational metrics This solution collects anonymous operational metrics to help AWS improve the quality of features of the solution. -For more information, including how to disable this capability, please see the [implementation guide](https://docs.aws.amazon.com/solutions/latest/maintaining-personalized-experiences-with-ml/collection-of-operational-metrics.html). +For more information, including how to disable this capability, please see the [implementation guide](https://docs.aws.amazon.com/solutions/latest/maintaining-personalized-experiences-with-ml/reference.html). --- diff --git a/source/aws_lambda/prepare_input/handler.py b/source/aws_lambda/prepare_input/handler.py index 159fde0..3b61252 100644 --- a/source/aws_lambda/prepare_input/handler.py +++ b/source/aws_lambda/prepare_input/handler.py @@ -23,7 +23,7 @@ metrics = Metrics() -def lambda_handler(event: Dict[str, Any], context: LambdaContext) -> Dict: +def lambda_handler(event: Dict[str, Any], _) -> Dict: """Add timeStarted to the workflowConfig of all items :param event: AWS Lambda Event :param context: AWS Lambda Context diff --git a/source/aws_lambda/shared/personalize_service.py b/source/aws_lambda/shared/personalize_service.py index bd96b9f..2aea466 100644 --- a/source/aws_lambda/shared/personalize_service.py +++ b/source/aws_lambda/shared/personalize_service.py @@ -65,7 +65,19 @@ ("timeStarted", Resource), ("solutionVersionArn", SolutionVersion), ) - +RESOURCE_TYPES = [ + "datasetGroup", + "datasetImport", + "dataset", + "eventTracker", + "solution", + "solutionVersion", + "filter", + "recommender", + "campaign", + "batchJob", + "segmentJob" +] def get_duplicates(items): if isinstance(items, str): @@ -714,71 +726,60 @@ def _validate_filters(self, path="filters[].serviceConfig"): self._fill_default_vals("filter", _filter) def _validate_type(self, var, typ, err: str): - validates = isinstance(var, typ) + validates = isinstance(var, typ) and var is not None + if not validates: self._configuration_errors.append(err) + return validates - def _validate_solutions(self, path="solutions[]"): + def _validate_solutions(self, path="solutions[]"): solutions = jmespath.search(path, self.config_dict) or {} - for idx, _solution in enumerate(solutions): - campaigns = _solution.get("campaigns", []) - if self._validate_type(campaigns, list, f"solutions[{idx}].campaigns must be a list"): - self._validate_campaigns(f"solutions[{idx}].campaigns", campaigns) - - batch_inference_jobs = _solution.get("batchInferenceJobs", []) - if batch_inference_jobs and self._validate_type( - batch_inference_jobs, - list, - f"solutions[{idx}].batchInferenceJobs must be a list", - ): - self._validate_batch_inference_jobs( - path=f"solutions[{idx}].batchInferenceJobs", - solution_name=_solution.get("serviceConfig", {}).get("name", ""), - batch_inference_jobs=batch_inference_jobs, - ) - batch_segment_jobs = _solution.get("batchSegmentJobs", []) - if batch_segment_jobs and self._validate_type( - batch_segment_jobs, - list, - f"solutions[{idx}].batchSegmentJobs must be a list", - ): - self._validate_batch_segment_jobs( - path=f"solutions[{idx}].batchSegmentJobs", - solution_name=_solution.get("serviceConfig", {}).get("name", ""), - batch_segment_jobs=batch_segment_jobs, - ) + for idx, _solution in enumerate(solutions): + # Validate campaigns and batch jobs + self._validate_campaigns(f"solutions[{idx}].campaigns", _solution.get("campaigns", [])) + self._validate_batch_inference_jobs( + path=f"solutions[{idx}].batchInferenceJobs", + solution_name=_solution.get("serviceConfig", {}).get("name", ""), + batch_inference_jobs=_solution.get("batchInferenceJobs", []), + ) + self._validate_batch_segment_jobs( + path=f"solutions[{idx}].batchSegmentJobs", + solution_name=_solution.get("serviceConfig", {}).get("name", ""), + batch_segment_jobs=_solution.get("batchSegmentJobs", []), + ) - _solution = _solution.get("serviceConfig") + # Validate service configuration + _service_config = _solution.get("serviceConfig") - if not self._validate_type(_solution, dict, f"solutions[{idx}].serviceConfig must be an object"): + if not self._validate_type(_service_config, dict, f"solutions[{idx}].serviceConfig must be an object"): continue # `performAutoML` is currently returned from InputValidator.validate() as a valid field # Once the botocore Stubber is updated to not have this param anymore in `create_solution` call, # this check can be deleted. - if "performAutoML" in _solution: - del _solution["performAutoML"] + if "performAutoML" in _service_config: + del _service_config["performAutoML"] logger.error( "performAutoML is not a valid configuration parameter - proceeding to create the " "solution without this feature. For more details, refer to the Maintaining Personalized Experiences " "Github project's README.md file." ) - _solution["datasetGroupArn"] = DatasetGroup().arn("validation") - if "solutionVersion" in _solution: - # To pass solution through InputValidator - solution_version_config = _solution["solutionVersion"] - del _solution["solutionVersion"] - self._validate_resource(Solution(), _solution) - _solution["solutionVersion"] = solution_version_config + _service_config["datasetGroupArn"] = DatasetGroup().arn("validation") + if "solutionVersion" in _service_config: + # To pass solution through InputValidator + solution_version_config = _service_config["solutionVersion"] + del _service_config["solutionVersion"] + self._validate_resource(Solution(), _service_config) + _service_config["solutionVersion"] = solution_version_config else: - self._validate_resource(Solution(), _solution) + self._validate_resource(Solution(), _service_config) - self._fill_default_vals("solution", _solution) - self._validate_solution_version(_solution) + self._fill_default_vals("solution", _service_config) + self._validate_solution_version(_service_config) def _validate_solution_version(self, solution_config): allowed_sol_version_keys = ["trainingMode", "tags"] @@ -819,6 +820,8 @@ def _validate_solution_update(self): ) def _validate_campaigns(self, path, campaigns: List[Dict]): + self._validate_type(campaigns, list, f"{path} must be a list") + for idx, campaign_config in enumerate(campaigns): current_path = f"{path}.campaigns[{idx}]" @@ -832,6 +835,12 @@ def _validate_campaigns(self, path, campaigns: List[Dict]): self._fill_default_vals("campaign", campaign) def _validate_batch_inference_jobs(self, path, solution_name, batch_inference_jobs: List[Dict]): + self._validate_type( + batch_inference_jobs, + list, + f"solutions[{path} must be a list", + ) + for idx, batch_job_config in enumerate(batch_inference_jobs): current_path = f"{path}.batchInferenceJobs[{idx}]" @@ -860,6 +869,12 @@ def _validate_batch_inference_jobs(self, path, solution_name, batch_inference_jo self._fill_default_vals("batchJob", batch_job) def _validate_batch_segment_jobs(self, path, solution_name, batch_segment_jobs: List[Dict]): + self._validate_type( + batch_segment_jobs, + list, + f"solutions[{path} must be a list", + ) + for idx, batch_job_config in enumerate(batch_segment_jobs): current_path = f"{path}.batchSegmentJobs[{idx}]" @@ -1108,42 +1123,23 @@ def _validate_naming(self): self._validate_no_duplicates(name="campaign names", path="solutions[].campaigns[].serviceConfig.name") self._validate_no_duplicates(name="solution names", path="solutions[].serviceConfig.name") - def _fill_default_vals(self, resource_type, resource_dict): - """Insert default values for tags and other fields whenever not supplied""" - - if ( - resource_type - in [ - "datasetGroup", - "datasetImport", - "dataset", - "eventTracker", - "solution", - "solutionVersion", - "filter", - "recommender", - "campaign", - "batchJob", - "segmentJob", - ] - and "tags" not in resource_dict - ): + def _fill_resource_dict_tags(self, resource_type, resource_dict): + if resource_type in RESOURCE_TYPES and "tags" not in resource_dict: if self.pass_root_tags: resource_dict["tags"] = self.config_dict["tags"] else: resource_dict["tags"] = [] + def _fill_default_vals(self, resource_type, resource_dict): + """Insert default values for tags and other fields whenever not supplied""" + self._fill_resource_dict_tags(resource_type, resource_dict) + if resource_type == "datasetImport": if "importMode" not in resource_dict: resource_dict["importMode"] = "FULL" + if "publishAttributionMetricsToS3" not in resource_dict: resource_dict["publishAttributionMetricsToS3"] = False - if resource_type == "solutionVersion": - if "tags" not in resource_dict: - if self.pass_root_tags: - resource_dict["tags"] = self.config_dict["tags"] - else: - resource_dict["tags"] = [] - if "trainingMode" not in resource_dict: - resource_dict["trainingMode"] = "FULL" + if resource_type == "solutionVersion" and "trainingMode" not in resource_dict: + resource_dict["trainingMode"] = "FULL" diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_hash/hash.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_hash/hash.py index 9b0819e..a7e7418 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_hash/hash.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_hash/hash.py @@ -21,6 +21,9 @@ from aws_solutions.cdk.aws_lambda.python.function import SolutionsPythonFunction from aws_solutions.cdk.cfn_nag import add_cfn_nag_suppressions, CfnNagSuppression +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression + class ResourceHash(Construct): """Used to create unique resource names based on the hash of the stack ID""" @@ -56,6 +59,15 @@ def __init__( ], ) + NagSuppressions.add_resource_suppressions(self._resource_name_function.role, [ + NagPackSuppression( + id='AwsSolutions-IAM5', + reason='All IAM policies defined in this solution' + 'grant only least-privilege permissions. Wild ' + 'card for resources is used only for services ' + 'which do not have a resource arn')], + apply_to_children=True) + properties = { "ServiceToken": self._resource_name_function.function_arn, "Purpose": purpose, diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_name/name.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_name/name.py index 02198ed..d540f79 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_name/name.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/resource_name/name.py @@ -23,6 +23,9 @@ from aws_solutions.cdk.aws_lambda.python.function import SolutionsPythonFunction from aws_solutions.cdk.cfn_nag import add_cfn_nag_suppressions, CfnNagSuppression +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression + class ResourceName(Construct): """Used to create unique resource names of the format {stack_name}-{purpose}-{id}""" @@ -59,6 +62,15 @@ def __init__( ], ) + NagSuppressions.add_resource_suppressions(self._resource_name_function.role, [ + NagPackSuppression( + id='AwsSolutions-IAM5', + reason='All IAM policies defined in this solution' + 'grant only least-privilege permissions. Wild ' + 'card for resources is used only for services ' + 'which do not have a resource arn')], + apply_to_children=True) + properties = { "ServiceToken": self._resource_name_function.function_arn, "Purpose": purpose, diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/solutions_metrics/src/custom_resources/requirements.txt b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/solutions_metrics/src/custom_resources/requirements.txt index 293fe27..0e04dcc 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/solutions_metrics/src/custom_resources/requirements.txt +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/cfn_custom_resources/solutions_metrics/src/custom_resources/requirements.txt @@ -1,3 +1,3 @@ requests==2.31.0 -urllib3==1.26.16 +urllib3==1.26.17 crhelper==2.0.11 diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py index 242074a..e151cd7 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/java/bundling.py @@ -44,7 +44,7 @@ def __init__( self.gradle_test = gradle_test self.distribution_path = distribution_path - def try_bundle(self, output_dir: str, options: BundlingOptions) -> bool: + def try_bundle(self, output_dir: str, options: BundlingOptions) -> bool: #NOSONAR - Options are required for method header source = Path(self.to_bundle).absolute() is_gradle_build = (source / "gradlew").exists() diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py index c86985c..bff7081 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/aws_lambda/python/bundling.py @@ -55,7 +55,7 @@ def platform_supports_bundling(self): logger.info("local bundling %s supported for %s" % ("is" if os_platform_can_bundle else "is not", os_platform)) return os_platform_can_bundle - def try_bundle(self, output_dir: str, options: BundlingOptions) -> bool: + def try_bundle(self, output_dir: str, options: BundlingOptions) -> bool: #NOSONAR - Options are required for method header if not self.platform_supports_bundling: raise SolutionsPythonBundlingException("this platform does not support bundling") diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/scripts/build_s3_cdk_dist.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/scripts/build_s3_cdk_dist.py index 1d63b81..3ed63ce 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/scripts/build_s3_cdk_dist.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/scripts/build_s3_cdk_dist.py @@ -173,22 +173,6 @@ def __init__(self, build_env: BuildEnvironment): def package(self): logger.info("packaging global assets") - -def validate_version_code(ctx, param, value): - """ - Version codes are validated as semantic versions prefixed by a v, e.g. v1.2.3 - :param ctx: the click context - :param param: the click parameter - :param value: the parameter value - :return: the validated value - """ - re_semver = r"^v(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" - if re.match(re_semver, value): - return value - else: - raise click.BadParameter("please specifiy major, minor and patch versions, e.g. v1.0.0") - - @click.group() @click.option( "--log-level", @@ -309,8 +293,7 @@ def source_code_package(ctx, ignore, solution_name): @click.option( "--version-code", help="The version of the package.", - required=True, - callback=validate_version_code, + required=True ) @click.option( "--cdk-app-path", diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stack.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stack.py index 4af2b71..2d5e7c2 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stack.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stack.py @@ -23,6 +23,9 @@ from aws_solutions.cdk.interfaces import TemplateOptions from aws_solutions.cdk.mappings import Mappings +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression + RE_SOLUTION_ID = re.compile(r"^SO\d+$") RE_TEMPLATE_FILENAME = re.compile(r"^[a-z]+(?:-[a-z]+)*\.template$") # NOSONAR @@ -51,6 +54,15 @@ def visit(self, node: IConstruct): if node == self.stack: self.stack.metrics = Metrics(self.stack, "Metrics", self.stack.metrics) + NagSuppressions.add_resource_suppressions(self.stack.metrics, [ + NagPackSuppression( + id='AwsSolutions-IAM5', + reason='All IAM policies defined in this solution' + 'grant only least-privilege permissions. Wild ' + 'card for resources is used only for services ' + 'which do not have a resource arn')], + apply_to_children=True) + class SolutionStack(Stack): def __init__( diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stepfunctions/solutionstep.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stepfunctions/solutionstep.py index 7683502..415bb8b 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stepfunctions/solutionstep.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/stepfunctions/solutionstep.py @@ -123,7 +123,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs): if libraries and any(not l.exists() for l in libraries): raise ValueError(f"libraries provided, but do not exist at {libraries}") - function = kwargs.pop("function") + function_name = kwargs.pop("function") kwargs["layers"] = kwargs.get("layers", []) kwargs["tracing"] = Tracing.ACTIVE kwargs["timeout"] = Duration.seconds(15) @@ -133,7 +133,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs): scope, construct_id, entrypoint, - function, + function_name, libraries=libraries, **kwargs, ) diff --git a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py index 705a90a..13b7cd5 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py +++ b/source/cdk_solution_helper_py/helpers_cdk/aws_solutions/cdk/synthesizers.py @@ -19,7 +19,7 @@ from dataclasses import dataclass, field from fileinput import FileInput from pathlib import Path -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Iterable import jsii from aws_cdk import DefaultStackSynthesizer, IStackSynthesizer, ISynthesisSession @@ -273,10 +273,10 @@ def _template_names(self, session: ISynthesisSession) -> List[Path]: templates.append(assembly_output_path.joinpath(child_template)) return templates - def _templates(self, session: ISynthesisSession) -> Tuple[Path, Dict]: + def _templates(self, session: ISynthesisSession) -> Iterable[Tuple[Path, Dict]]: assembly_output_path = Path(session.assembly.outdir) - assets = {} + try: assets = json.loads(next(assembly_output_path.glob(self._stack.stack_name + "*.assets.json")).read_text()) except StopIteration: diff --git a/source/cdk_solution_helper_py/helpers_cdk/setup.py b/source/cdk_solution_helper_py/helpers_cdk/setup.py index 546de5a..f59c70b 100644 --- a/source/cdk_solution_helper_py/helpers_cdk/setup.py +++ b/source/cdk_solution_helper_py/helpers_cdk/setup.py @@ -50,7 +50,7 @@ def get_version(): }, install_requires=[ "pip>=22.3.1", - "aws_cdk_lib==2.75.0", + "aws_cdk_lib==2.88.0", "Click==8.1.3", "boto3==1.26.47", "requests==2.31.0", diff --git a/source/cdk_solution_helper_py/helpers_common/aws_solutions/core/config.py b/source/cdk_solution_helper_py/helpers_common/aws_solutions/core/config.py index b9dde60..7349c48 100644 --- a/source/cdk_solution_helper_py/helpers_common/aws_solutions/core/config.py +++ b/source/cdk_solution_helper_py/helpers_common/aws_solutions/core/config.py @@ -23,9 +23,6 @@ SOLUTION_ID_RE = re.compile(r"^SO(?P\d+)(?P[a-zA-Z]*)$") # NOSONAR -SOLUTION_VERSION_RE = re.compile( - r"^v(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" # NOSONAR -) class SolutionConfigEnv: @@ -55,7 +52,7 @@ class Config: """Stores information about the current solution""" id = SolutionConfigEnv("SOLUTION_ID", regex=SOLUTION_ID_RE) - version = SolutionConfigEnv("SOLUTION_VERSION", regex=SOLUTION_VERSION_RE) + version = SolutionConfigEnv("SOLUTION_VERSION", regex=None) _botocore_config = None @property diff --git a/source/cdk_solution_helper_py/requirements-dev.txt b/source/cdk_solution_helper_py/requirements-dev.txt index 5dc819e..5637c22 100644 --- a/source/cdk_solution_helper_py/requirements-dev.txt +++ b/source/cdk_solution_helper_py/requirements-dev.txt @@ -1,17 +1,17 @@ -aws_cdk_lib==2.75.0 -aws-cdk.aws-servicecatalogappregistry-alpha==2.75.0a0 +aws_cdk_lib==2.88.0 +aws-cdk.aws-servicecatalogappregistry-alpha==2.88.0a0 black boto3==1.26.47 requests==2.31.0 crhelper==2.0.11 -Click -moto +Click~=8.1.3 +moto~=2.3.0 pipenv -poetry -pytest>=7.2.0 +poetry~=1.6.1 +pytest>=7.4.2 pytest-cov>=4.0.0 pytest-mock>=3.10.0 -tox +tox~=2.9.1 tox-pyenv -e ./source/cdk_solution_helper_py/helpers_cdk -e ./source/cdk_solution_helper_py/helpers_common diff --git a/source/images/solution-architecture.jpg b/source/images/solution-architecture.jpg deleted file mode 100644 index 7634f22..0000000 Binary files a/source/images/solution-architecture.jpg and /dev/null differ diff --git a/source/infrastructure/aspects/app_registry.py b/source/infrastructure/aspects/app_registry.py index c17967c..2d9d61c 100644 --- a/source/infrastructure/aspects/app_registry.py +++ b/source/infrastructure/aspects/app_registry.py @@ -41,7 +41,7 @@ def visit(self, node: IConstruct) -> None: # parent stack stack: cdk.Stack = node self.__create_app_for_app_registry() - self.application.associate_stack(stack) + self.application.associate_application_with_stack(stack) self.__create_atttribute_group() self.__add_tags_for_application() else: @@ -49,7 +49,7 @@ def visit(self, node: IConstruct) -> None: if not self.application: self.__create_app_for_app_registry() - self.application.associate_stack(node) + self.application.associate_application_with_stack(node) def __create_app_for_app_registry(self) -> None: """Method to create an AppRegistry Application""" @@ -77,7 +77,9 @@ def __create_atttribute_group(self) -> None: if not self.application: self.__create_app_for_app_registry() - self.application.associate_attribute_group( + # The AttributeGroup.associate_with takes 2 params. First is Attribute Group, second is application. + # The method signature unclear in official documentation. + appreg.AttributeGroup.associate_with( appreg.AttributeGroup( self, "AppAttributes", @@ -90,4 +92,5 @@ def __create_atttribute_group(self) -> None: "solutionName": self.solution_name, }, ) + , self.application ) diff --git a/source/infrastructure/cdk.json b/source/infrastructure/cdk.json index 690b99d..81df1fd 100644 --- a/source/infrastructure/cdk.json +++ b/source/infrastructure/cdk.json @@ -3,7 +3,7 @@ "context": { "SOLUTION_NAME": "Maintaining Personalized Experiences with Machine Learning", "SOLUTION_ID": "SO0170", - "SOLUTION_VERSION": "v1.4.2", + "SOLUTION_VERSION": "v1.4.3", "APP_REGISTRY_NAME": "personalized-experiences-ML", "APPLICATION_TYPE": "AWS-Solutions", "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true diff --git a/source/infrastructure/deploy.py b/source/infrastructure/deploy.py index 6f1f4d4..3587f85 100644 --- a/source/infrastructure/deploy.py +++ b/source/infrastructure/deploy.py @@ -20,6 +20,9 @@ from aspects.app_registry import AppRegistry from aws_solutions.cdk import CDKSolution from personalize.stack import PersonalizeStack +from cdk_nag import AwsSolutionsChecks +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression logger = logging.getLogger("cdk-helper") solution = CDKSolution(cdk_json_path=Path(__file__).parent.absolute() / "cdk.json") @@ -40,6 +43,15 @@ def build_app(context): ) cdk.Aspects.of(app).add(AppRegistry(stack, "AppRegistryAspect")) + cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) + + NagSuppressions.add_stack_suppressions(stack, + [ + NagPackSuppression(id='AwsSolutions-L1', + reason='Python lambda runtime is planned to be ' + 'upgraded in future release.') + ]) + return app.synth() diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py b/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py index ef876b2..eebc0a1 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_batch_inference_job.py @@ -17,6 +17,9 @@ from aws_cdk.aws_s3 import IBucket from constructs import Construct +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression + from aws_solutions.cdk.stepfunctions.solutionstep import SolutionStep @@ -61,6 +64,16 @@ def __init__( ) ) + NagSuppressions.add_resource_suppressions( + self.personalize_batch_inference_rw_role, [ + NagPackSuppression( + id='AwsSolutions-IAM5', + reason='All IAM policies defined in this solution' + 'grant only least-privilege permissions. Wild ' + 'card for resources is used only for services ' + 'which do not have a resource arn')], + apply_to_children=True) + super().__init__( scope, id, diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py b/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py index 99b62fc..6e83fee 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_batch_segment_job.py @@ -17,6 +17,9 @@ from aws_cdk.aws_s3 import IBucket from constructs import Construct +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression + from aws_solutions.cdk.stepfunctions.solutionstep import SolutionStep @@ -61,6 +64,16 @@ def __init__( ) ) + NagSuppressions.add_resource_suppressions( + self.personalize_batch_inference_rw_role, [ + NagPackSuppression( + id='AwsSolutions-IAM5', + reason='All IAM policies defined in this solution' + 'grant only least-privilege permissions. Wild ' + 'card for resources is used only for services ' + 'which do not have a resource arn')], + apply_to_children=True) + super().__init__( scope, id, diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_config.py b/source/infrastructure/personalize/aws_lambda/functions/create_config.py index 08b083d..298f4ae 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_config.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_config.py @@ -26,13 +26,13 @@ class CreateConfig(SolutionsPythonFunction): def __init__(self, scope: Construct, construct_id: str, **kwargs): entrypoint = Path(__file__).absolute().parents[4] / "aws_lambda" / "create_config" / "handler.py" - function = "lambda_handler" + function_name = "lambda_handler" kwargs["libraries"] = [Path(__file__).absolute().parents[4] / "aws_lambda" / "shared"] kwargs["tracing"] = Tracing.ACTIVE kwargs["timeout"] = Duration.seconds(90) kwargs["runtime"] = Runtime("python3.9", RuntimeFamily.PYTHON) - super().__init__(scope, construct_id, entrypoint, function, **kwargs) + super().__init__(scope, construct_id, entrypoint, function_name, **kwargs) self.environment = Environment(self) diff --git a/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py b/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py index c1803a9..55b1e9f 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py +++ b/source/infrastructure/personalize/aws_lambda/functions/create_dataset_import_job.py @@ -19,6 +19,9 @@ from aws_cdk.aws_stepfunctions import IChainable from constructs import Construct +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression + from aws_solutions.cdk.stepfunctions.solutionstep import SolutionStep @@ -70,6 +73,16 @@ def __init__( ) ) + NagSuppressions.add_resource_suppressions( + self.personalize_role, [ + NagPackSuppression( + id='AwsSolutions-IAM5', + reason='All IAM policies defined in this solution' + 'grant only least-privilege permissions. Wild ' + 'card for resources is used only for services ' + 'which do not have a resource arn')], + apply_to_children=True) + super().__init__( scope, id, diff --git a/source/infrastructure/personalize/aws_lambda/functions/s3_event.py b/source/infrastructure/personalize/aws_lambda/functions/s3_event.py index 027dd55..5b4d6bd 100644 --- a/source/infrastructure/personalize/aws_lambda/functions/s3_event.py +++ b/source/infrastructure/personalize/aws_lambda/functions/s3_event.py @@ -30,13 +30,13 @@ def __init__( self, scope: Construct, construct_id: str, state_machine: StateMachine, bucket: Bucket, topic: Topic, **kwargs ): entrypoint = Path(__file__).absolute().parents[4] / "aws_lambda" / "s3_event" / "handler.py" - function = "lambda_handler" + function_name = "lambda_handler" kwargs["libraries"] = [Path(__file__).absolute().parents[4] / "aws_lambda" / "shared"] kwargs["tracing"] = Tracing.ACTIVE kwargs["timeout"] = Duration.seconds(15) kwargs["runtime"] = Runtime("python3.9", RuntimeFamily.PYTHON) - super().__init__(scope, construct_id, entrypoint, function, **kwargs) + super().__init__(scope, construct_id, entrypoint, function_name, **kwargs) self.environment = Environment(self) self.add_environment("STATE_MACHINE_ARN", state_machine.state_machine_arn) diff --git a/source/infrastructure/personalize/stack.py b/source/infrastructure/personalize/stack.py index 92ca482..d2f3abd 100644 --- a/source/infrastructure/personalize/stack.py +++ b/source/infrastructure/personalize/stack.py @@ -69,6 +69,9 @@ from personalize.step_functions.schedules import Schedules from personalize.step_functions.solution_fragment import SolutionFragment +from cdk_nag import NagSuppressions +from cdk_nag import NagPackSuppression + class PersonalizeStack(SolutionStack): def __init__(self, scope: Construct, construct_id: str, *args, **kwargs) -> None: @@ -120,6 +123,7 @@ def __init__(self, scope: Construct, construct_id: str, *args, **kwargs) -> None access_logs_bucket = AccessLogsBucket( self, "AccessLogsBucket", + enforce_ssl=True, suppress=[ CfnNagSuppression( "W35", @@ -133,6 +137,7 @@ def __init__(self, scope: Construct, construct_id: str, *args, **kwargs) -> None "PersonalizeBucket", server_access_logs_bucket=access_logs_bucket, server_access_logs_prefix="personalize-bucket-access-logs/", + enforce_ssl=True ) # the AWS lambda functions required by the shared step functions @@ -371,6 +376,13 @@ def __init__(self, scope: Construct, construct_id: str, *args, **kwargs) -> None ], ) + NagSuppressions.add_resource_suppressions( + [dataset_import_schedule_sfn, solution_maintenance_schedule_sfn, + scheduler.state_machine, state_machine], + [NagPackSuppression(id='AwsSolutions-SF1', + reason='Information required for troubleshooting is ' + 'logged by state function and lambda functions')]) + s3_event_handler = S3EventHandler( self, "S3EventHandler", @@ -429,6 +441,35 @@ def __init__(self, scope: Construct, construct_id: str, *args, **kwargs) -> None ) ) + NagSuppressions.add_resource_suppressions([create_config.role, prepare_input, create_dataset_group, + create_schema, create_dataset, create_dataset_import_job, + notifications, create_event_tracker, create_solution, + create_recommender, create_solution_version, create_campaign, + create_batch_inference_job, create_batch_segment_job, + create_filter, create_timestamp, dataset_import_schedule_sfn, + solution_maintenance_schedule_sfn, scheduler.read_scheduled_task, + scheduler.update_scheduled_task, scheduler.create_scheduled_task, + scheduler.delete_scheduled_task, scheduler.state_machine, + scheduler.scheduler_function.role, + state_machine, create_dataset_group, + s3_event_handler.role, bucket_notification_handler], + [ + NagPackSuppression(id='AwsSolutions-IAM5', + reason='All IAM policies defined in this ' + 'solution' + 'grant only least-privilege ' + 'permissions. Wild' + 'card for resources is used only for ' + 'services' + 'which do not have a resource arn'), + NagPackSuppression(id='AwsSolutions-IAM4', + reason='We use AWS managed policies and do ' + 'not need to' + 'change as the "*" in resource is due ' + 'to circular dependency.') + ], + apply_to_children=True) + # dashboard self.dashboard = Dashboard( self, diff --git a/source/infrastructure/personalize/step_functions/scheduled_dataset_import.py b/source/infrastructure/personalize/step_functions/scheduled_dataset_import.py index afcd142..a7b5572 100644 --- a/source/infrastructure/personalize/step_functions/scheduled_dataset_import.py +++ b/source/infrastructure/personalize/step_functions/scheduled_dataset_import.py @@ -38,6 +38,7 @@ def __init__( self.state_machine = StateMachine( self, "PeriodicDatasetImport", + tracing_enabled=True, state_machine_name=state_machine_namer.resource_name.to_string(), definition=Chain.start( Parallel(self, "Manage The Execution") diff --git a/source/infrastructure/personalize/step_functions/scheduled_solution_maintenance.py b/source/infrastructure/personalize/step_functions/scheduled_solution_maintenance.py index 33de64f..c286ba6 100644 --- a/source/infrastructure/personalize/step_functions/scheduled_solution_maintenance.py +++ b/source/infrastructure/personalize/step_functions/scheduled_solution_maintenance.py @@ -56,6 +56,7 @@ def __init__( self.state_machine = StateMachine( self, "PeriodicSolutionMaintenance", + tracing_enabled=True, state_machine_name=state_machine_namer.resource_name.to_string(), definition=Chain.start( Parallel(self, "Manage Solution Maintenance") diff --git a/source/infrastructure/setup.py b/source/infrastructure/setup.py index d606b00..30ddfa9 100644 --- a/source/infrastructure/setup.py +++ b/source/infrastructure/setup.py @@ -35,7 +35,7 @@ author="AWS Solutions Builders", packages=setuptools.find_packages(), install_requires=[ - "aws-cdk-lib==2.75.0", + "aws-cdk-lib==2.88.0", "pip>=22.3.1", ], python_requires=">=3.9", diff --git a/source/requirements-dev.txt b/source/requirements-dev.txt index 7084bdf..1c9c71e 100644 --- a/source/requirements-dev.txt +++ b/source/requirements-dev.txt @@ -1,19 +1,20 @@ avro==1.11.1 black boto3==1.26.47 -aws_cdk_lib==2.75.0 -aws_solutions_constructs.aws_lambda_sns==2.38.0 -aws-cdk.aws-servicecatalogappregistry-alpha==2.75.0a0 +aws_cdk_lib==2.88.0 +aws_solutions_constructs.aws_lambda_sns==2.41.0 +aws-cdk.aws-servicecatalogappregistry-alpha==2.88.0a0 +cdk-nag==2.27.107 requests==2.31.0 crhelper==2.0.11 cronex==0.1.3.1 moto==2.3.0 parsedatetime==2.6 -pytest>=7.2.0 +pytest>=7.4.2 pytest-cov>=4.0.0 pytest-env>=0.8.1 pytest-mock>=3.10.0 -pyyaml==5.4.1 +pyyaml==6.0 responses~=0.17.0 tenacity==8.0.1 -e cdk_solution_helper_py/helpers_cdk diff --git a/source/scheduler/cdk/aws_solutions/scheduler/cdk/aws_lambda/get_next_scheduled_event/src/main/java/com/amazonaws/solutions/schedule_sfn_task/ScheduleEvent.java b/source/scheduler/cdk/aws_solutions/scheduler/cdk/aws_lambda/get_next_scheduled_event/src/main/java/com/amazonaws/solutions/schedule_sfn_task/ScheduleEvent.java index e03e0b2..a645b2e 100644 --- a/source/scheduler/cdk/aws_solutions/scheduler/cdk/aws_lambda/get_next_scheduled_event/src/main/java/com/amazonaws/solutions/schedule_sfn_task/ScheduleEvent.java +++ b/source/scheduler/cdk/aws_solutions/scheduler/cdk/aws_lambda/get_next_scheduled_event/src/main/java/com/amazonaws/solutions/schedule_sfn_task/ScheduleEvent.java @@ -17,9 +17,6 @@ public class ScheduleEvent { private String schedule; private String next; - public ScheduleEvent() { - } - public String getNext() { return next; } diff --git a/source/scheduler/cdk/aws_solutions/scheduler/cdk/construct.py b/source/scheduler/cdk/aws_solutions/scheduler/cdk/construct.py index 1c33117..9be461a 100644 --- a/source/scheduler/cdk/aws_solutions/scheduler/cdk/construct.py +++ b/source/scheduler/cdk/aws_solutions/scheduler/cdk/construct.py @@ -71,7 +71,7 @@ def visit(self, node: IConstruct): self.scheduler.state_machine.add_to_role_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, - resources=["*"], + resources=[self.scheduler.state_machine_executions_arn], actions=["states:DescribeExecution", "states:StopExecution"], ) ) @@ -376,7 +376,7 @@ def _scheduler_function(self, scope: Construct, construct_id: str) -> SolutionsJ project_path = Path(__file__).absolute().parents[1] / "cdk" / "aws_lambda" / "get_next_scheduled_event" distribution_path = project_path / "build" / "distributions" - function = SolutionsJavaFunction( + solutions_java_function = SolutionsJavaFunction( scope=scope, construct_id=construct_id, handler="com.amazonaws.solutions.schedule_sfn_task.HandleScheduleEvent", @@ -386,8 +386,10 @@ def _scheduler_function(self, scope: Construct, construct_id: str) -> SolutionsJ distribution_path=distribution_path, tracing=Tracing.ACTIVE, ) + add_cfn_nag_suppressions( - function.role.node.try_find_child("DefaultPolicy").node.find_child("Resource"), + solutions_java_function.role.node.try_find_child("DefaultPolicy").node.find_child("Resource"), [CfnNagSuppression("W12", "IAM policy for AWS X-Ray requires an allow on *")], ) - return function + + return solutions_java_function diff --git a/source/scheduler/cdk/setup.py b/source/scheduler/cdk/setup.py index 906eff5..430385c 100644 --- a/source/scheduler/cdk/setup.py +++ b/source/scheduler/cdk/setup.py @@ -43,7 +43,7 @@ def get_version(): packages=setuptools.find_namespace_packages(exclude=["build*"]), install_requires=[ "pip>=22.3.1", - "aws_cdk_lib==2.75.0", + "aws_cdk_lib==2.88.0", "Click==8.1.3", "boto3==1.26.47", ], diff --git a/source/tests/aspects/test_personalize_app_stack.py b/source/tests/aspects/test_personalize_app_stack.py index ee9bd09..7e5993b 100644 --- a/source/tests/aspects/test_personalize_app_stack.py +++ b/source/tests/aspects/test_personalize_app_stack.py @@ -67,11 +67,11 @@ def test_service_catalog_registry_application(synth_template): "Tags": { "SOLUTION_ID": "SO0170", "SOLUTION_NAME": "Maintaining Personalized Experiences with Machine Learning", - "SOLUTION_VERSION": "v1.4.2", + "SOLUTION_VERSION": "v1.4.3", "Solutions:ApplicationType": "AWS-Solutions", "Solutions:SolutionID": "SO0170", "Solutions:SolutionName": "Maintaining Personalized Experiences with Machine Learning", - "Solutions:SolutionVersion": "v1.4.2", + "Solutions:SolutionVersion": "v1.4.3", }, }, ) diff --git a/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml b/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml index 2c1c716..fcf88d4 100644 --- a/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml +++ b/source/tests/cdk_solution_helper/aws_lambda/python/fixtures/pyproject.toml @@ -9,6 +9,7 @@ python = "^3.9" minimal = {path = "package"} [tool.poetry.dev-dependencies] +pytest = "^7.4.2" [build-system] requires = ["poetry-core>=1.2.0"] diff --git a/source/tests/cdk_solution_helper/test_build_s3_cdk_dist.py b/source/tests/cdk_solution_helper/test_build_s3_cdk_dist.py index 68af41e..38afe4c 100644 --- a/source/tests/cdk_solution_helper/test_build_s3_cdk_dist.py +++ b/source/tests/cdk_solution_helper/test_build_s3_cdk_dist.py @@ -23,7 +23,6 @@ BuildEnvironment, RegionalAssetPackager, GlobalAssetPackager, - validate_version_code, BaseAssetPackager, ) @@ -63,16 +62,6 @@ def test_build_environment(): assert Path(build.source_dir).stem == "source" assert Path(build.infrastructure_dir).parent.stem == "source" - -def test_validate_version_code_valid(): - assert validate_version_code(None, None, TEST_VERSION_CODE) == TEST_VERSION_CODE - - -def test_validate_version_code_invalid(): - with pytest.raises(click.BadParameter): - assert validate_version_code(None, None, "1.0.0") - - @pytest.mark.parametrize( "packager_cls,expected_s3_path", [ diff --git a/source/tests/cdk_solution_helper/test_solution_config.py b/source/tests/cdk_solution_helper/test_solution_config.py index 38f0271..acf97c9 100644 --- a/source/tests/cdk_solution_helper/test_solution_config.py +++ b/source/tests/cdk_solution_helper/test_solution_config.py @@ -47,6 +47,7 @@ def solution_id_invalid(request): del os.environ["SOLUTION_ID"] +# Added before invalid version to valid version test as we removed the version check as we also test mainline in NW. @pytest.fixture( params=[ "v1.0.0-alpha", @@ -66,6 +67,10 @@ def solution_id_invalid(request): "v1.0.0+20130313144700", "v1.0.0-beta+exp.sha.5114f85", "v1.0.0+21AF26D3--117B344092BD", + "a.b.c", + "a1.2.3", + "v.1.2.3", + "mainline" ] ) def solution_version_valid(request): @@ -75,19 +80,10 @@ def solution_version_valid(request): del os.environ["SOLUTION_VERSION"] -@pytest.fixture(params=["a.b.c", "a1.2.3", "v.1.2.3"]) -def solution_version_invalid(request): - solution_version = request.param - os.environ["SOLUTION_VERSION"] = solution_version - yield solution_version - del os.environ["SOLUTION_VERSION"] - - def test_valid_solution_id(solution_id_valid): config_id = aws_solutions.core.config.id assert config_id == solution_id_valid - def test_invalid_solution_id(solution_id_invalid): with pytest.raises(ValueError): aws_solutions.core.config.id @@ -98,10 +94,7 @@ def test_valid_solution_version(solution_version_valid): assert version == solution_version_valid -def test_invalid_solution_id(solution_version_invalid): - with pytest.raises(ValueError): - aws_solutions.core.config.version - +# Removing test_invalid_solution_id test as we removed the version check as we also test mainline in NW. def test_valid_botocore_config(solution_id_valid, solution_version_valid): boto_config = aws_solutions.core.config.botocore_config diff --git a/source/tests/test_deploy.py b/source/tests/test_deploy.py index a7bf24f..494ec85 100644 --- a/source/tests/test_deploy.py +++ b/source/tests/test_deploy.py @@ -98,7 +98,8 @@ def test_access_logs_bucket(build_stacks_for_buckets): assert bucket_policy["Type"] == "AWS::S3::BucketPolicy" access_logs_policy_statements = bucket_policy["Properties"]["PolicyDocument"]["Statement"] - assert len(access_logs_policy_statements) == 2 + assert len(access_logs_policy_statements) == 3 + # Len increases by 1 after enabling enforce_ssl to True for policy in access_logs_policy_statements: if "Sid" in policy and policy["Sid"] == "HttpsOnly": @@ -108,6 +109,12 @@ def test_access_logs_bucket(build_stacks_for_buckets): assert policy["Effect"] == "Deny" assert policy["Resource"] == {"Fn::Join": ["", [{"Fn::GetAtt": ["AccessLogsBucket", "Arn"]}, "/*"]]} + elif "Action" in policy and policy["Action"] == "s3:*": # Policy for enforce_sll = True + assert policy["Principal"] == {"AWS": "*"} + assert policy["Condition"]["Bool"]["aws:SecureTransport"] == "false" + assert policy["Effect"] == "Deny" + assert policy["Resource"] == [{"Fn::GetAtt": ["AccessLogsBucket", "Arn"]}, {"Fn::Join": ["", [{"Fn::GetAtt": ["AccessLogsBucket", "Arn"]}, "/*"]]}] + else: assert policy["Principal"] == {"Service": "logging.s3.amazonaws.com"} assert policy["Action"] == "s3:PutObject"