example_resources_stack.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import os
  7. from constructs import Construct
  8. from aws_cdk import (
  9. CfnOutput,
  10. Fn,
  11. RemovalPolicy,
  12. Stack,
  13. aws_lambda as lambda_,
  14. aws_iam as iam,
  15. aws_s3 as s3,
  16. aws_s3_deployment as s3_deployment,
  17. aws_dynamodb as dynamo,
  18. )
  19. from .auth import AuthPolicy
  20. class ExampleResources(Stack):
  21. """
  22. Defines a set of resources to use with AWSCore's ScriptBehaviours and examples. The example resources are:
  23. * An S3 bucket with a text file
  24. * A python 'echo' lambda
  25. * A small dynamodb table with a primary 'id': str key
  26. """
  27. def __init__(self, scope: Construct, id_: str, project_name: str, feature_name: str, **kwargs) -> None:
  28. super().__init__(scope, id_, **kwargs,
  29. description=f'Contains resources for the AWSCore examples as part of the '
  30. f'{project_name} project')
  31. self._project_name = project_name
  32. self._feature_name = feature_name
  33. self._policy = AuthPolicy(context=self).generate_admin_policy(stack=self)
  34. self._s3_bucket = self.__create_s3_bucket()
  35. self._lambda = self.__create_example_lambda()
  36. self._table = self.__create_dynamodb_table()
  37. self.__create_outputs()
  38. # Finally grant cross stack references
  39. self.__grant_access()
  40. def __grant_access(self):
  41. user_group = iam.Group.from_group_arn(
  42. self,
  43. f'{self._project_name}-{self._feature_name}-ImportedUserGroup',
  44. Fn.import_value(f'{self._project_name}:UserGroup')
  45. )
  46. admin_group = iam.Group.from_group_arn(
  47. self,
  48. f'{self._project_name}-{self._feature_name}-ImportedAdminGroup',
  49. Fn.import_value(f'{self._project_name}:AdminGroup')
  50. )
  51. # Provide the admin and user groups permissions to read the example S3 bucket.
  52. # Cannot use the grant_read method defined by the Bucket structure since the method tries to add to
  53. # the resource-based policy but the imported IAM groups (which are tokens from Fn.ImportValue) are
  54. # not valid principals in S3 bucket policies.
  55. # Check https://aws.amazon.com/premiumsupport/knowledge-center/s3-invalid-principal-in-policy-error/
  56. user_group.add_to_principal_policy(
  57. iam.PolicyStatement(
  58. actions=[
  59. "s3:GetBucket*",
  60. "s3:GetObject*",
  61. "s3:List*"
  62. ],
  63. effect=iam.Effect.ALLOW,
  64. resources=[self._s3_bucket.bucket_arn, f'{self._s3_bucket.bucket_arn}/*']
  65. )
  66. )
  67. admin_group.add_to_principal_policy(
  68. iam.PolicyStatement(
  69. actions=[
  70. "s3:GetBucket*",
  71. "s3:GetObject*",
  72. "s3:List*"
  73. ],
  74. effect=iam.Effect.ALLOW,
  75. resources=[self._s3_bucket.bucket_arn, f'{self._s3_bucket.bucket_arn}/*']
  76. )
  77. )
  78. # Provide the admin and user groups permissions to invoke the example Lambda function.
  79. # Cannot use the grant_invoke method defined by the Function structure since the method tries to add to
  80. # the resource-based policy but the imported IAM groups (which are tokens from Fn.ImportValue) are
  81. # not valid principals in Lambda function policies.
  82. user_group.add_to_principal_policy(
  83. iam.PolicyStatement(
  84. actions=[
  85. "lambda:InvokeFunction"
  86. ],
  87. effect=iam.Effect.ALLOW,
  88. resources=[self._lambda.function_arn]
  89. )
  90. )
  91. admin_group.add_to_principal_policy(
  92. iam.PolicyStatement(
  93. actions=[
  94. "lambda:InvokeFunction"
  95. ],
  96. effect=iam.Effect.ALLOW,
  97. resources=[self._lambda.function_arn]
  98. )
  99. )
  100. # Provide the admin and user groups permissions to read from the DynamoDB table.
  101. self._table.grant_read_data(user_group)
  102. self._table.grant_read_data(admin_group)
  103. def __create_s3_bucket(self) -> s3.Bucket:
  104. # Create a sample S3 bucket following S3 best practices
  105. # # See https://docs.aws.amazon.com/AmazonS3/latest/dev/security-best-practices.html
  106. # 1. Block all public access to the bucket
  107. # 2. Use SSE-S3 encryption. Explore encryption at rest options via
  108. # https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html
  109. # 3. Enable Amazon S3 server access logging
  110. # https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html
  111. server_access_logs_bucket = None
  112. if self.node.try_get_context('disable_access_log') != 'true':
  113. server_access_logs_bucket = s3.Bucket.from_bucket_name(
  114. self,
  115. f'{self._project_name}-{self._feature_name}-ImportedAccessLogsBucket',
  116. Fn.import_value(f"{self._project_name}:ServerAccessLogsBucket")
  117. )
  118. # Auto cleanup bucket and data if requested
  119. _remove_storage = self.node.try_get_context('remove_all_storage_on_destroy') == 'true'
  120. _removal_policy = RemovalPolicy.DESTROY if _remove_storage else RemovalPolicy.RETAIN
  121. example_bucket = s3.Bucket(
  122. self,
  123. f'{self._project_name}-{self._feature_name}-Example-S3bucket',
  124. auto_delete_objects=_remove_storage,
  125. block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
  126. encryption=s3.BucketEncryption.S3_MANAGED,
  127. removal_policy=_removal_policy,
  128. server_access_logs_bucket=
  129. server_access_logs_bucket if server_access_logs_bucket else None,
  130. server_access_logs_prefix=
  131. f'{self._project_name}-{self._feature_name}-{self.region}-AccessLogs' if server_access_logs_bucket else None
  132. )
  133. s3_deployment.BucketDeployment(
  134. self,
  135. f'{self._project_name}-{self._feature_name}-S3bucket-Deployment',
  136. destination_bucket=example_bucket,
  137. sources=[
  138. s3_deployment.Source.asset('example/s3_content')
  139. ],
  140. retain_on_delete=False
  141. )
  142. return example_bucket
  143. def __create_example_lambda(self) -> lambda_.Function:
  144. # create lambda function
  145. function = lambda_.Function(
  146. self,
  147. f'{self._project_name}-{self._feature_name}-Lambda-Function',
  148. runtime=lambda_.Runtime.PYTHON_3_9,
  149. handler="lambda-handler.main",
  150. code=lambda_.Code.from_asset(os.path.join(os.path.dirname(__file__), 'lambda'))
  151. )
  152. return function
  153. def __create_dynamodb_table(self) -> dynamo.Table:
  154. # create dynamo table
  155. # NB: CDK does not support seeding data, see simple table_seeder.py
  156. demo_table = dynamo.Table(
  157. self,
  158. f'{self._project_name}-{self._feature_name}-Table',
  159. partition_key=dynamo.Attribute(
  160. name="id",
  161. type=dynamo.AttributeType.STRING
  162. )
  163. )
  164. # Auto-delete the table when requested
  165. if self.node.try_get_context('remove_all_storage_on_destroy') == 'true':
  166. demo_table.apply_removal_policy(RemovalPolicy.DESTROY)
  167. return demo_table
  168. def __create_outputs(self) -> None:
  169. # Define exports
  170. # Export resource group
  171. self._s3_output = CfnOutput(
  172. self,
  173. id=f'ExampleBucketOutput',
  174. description='An example S3 bucket name to use with AWSCore ScriptBehaviors',
  175. export_name=f"{self.stack_name}:ExampleS3Bucket",
  176. value=self._s3_bucket.bucket_name)
  177. # Define exports
  178. # Export resource group
  179. self._lambda_output = CfnOutput(
  180. self,
  181. id=f'ExampleLambdaOutput',
  182. description='An example Lambda name to use with AWSCore ScriptBehaviors',
  183. export_name=f"{self.stack_name}::ExampleLambdaFunction",
  184. value=self._lambda.function_name)
  185. # Export DynamoDB Table
  186. self._table_output = CfnOutput(
  187. self,
  188. id=f'ExampleDynamoTableOutput',
  189. description='An example DynamoDB Table name to use with AWSCore ScriptBehaviors',
  190. export_name=f"{self.stack_name}:ExampleTable",
  191. value=self._table.table_name)
  192. # Export user policy
  193. self._user_policy = CfnOutput(
  194. self,
  195. id=f'ExampleUserPolicyOutput',
  196. description='A User policy to invoke example resources',
  197. export_name=f"{self.stack_name}:ExampleUserPolicy",
  198. value=self._policy.managed_policy_arn)