def base_iot_rule(construct, action_property, **kwargs): """ Function that generates an IoT Rule. :param construct: Custom construct that will use this function. From the external construct is usually 'self'. :param action_property: Action property for the resources that the Rule will interact. :param kwargs: Consist of required 'rule_name', 'rule_disabled', 'sql', 'aws_iot_sql_version' and optional 'description'. :return: IoT Rule Construct. """ rule_payload = iot.CfnTopicRule.TopicRulePayloadProperty( actions=[action_property], rule_disabled=kwargs["rule_disabled"], sql=kwargs["sql"], aws_iot_sql_version=kwargs["aws_iot_sql_version"], description=kwargs.get("description"), ) # Defining AWS IoT Rule rule_name = construct.prefix + "_" + kwargs[ "rule_name"] + "_" + construct.environment_ rule_name = rule_name.replace("-", "_") iot_rule = iot.CfnTopicRule(construct, id=rule_name, rule_name=rule_name, topic_rule_payload=rule_payload) return iot_rule
def __init__(self, app: App, id: str) -> None: super().__init__(app, id) # Lambda Function with open("lambda-handler.py", encoding="utf8") as fp: handler_code = fp.read() lambdaFn = _lambda.Function( self, "IoTTriggerLambda", code=_lambda.InlineCode(handler_code), handler="index.main", timeout=Duration.seconds(10), runtime=_lambda.Runtime.PYTHON_3_9, ) # Set Lambda Logs Retention and Removal Policy logs.LogGroup(self, 'logs', log_group_name=f"/aws/lambda/{lambdaFn.function_name}", removal_policy=RemovalPolicy.DESTROY, retention=logs.RetentionDays.ONE_DAY) # IoT Thing iot_thing = iot.CfnThing(self, "IoTThing", thing_name="MyIotThing") # IoT Rule with SQL, which invokes a Lambda Function iot_topic_rule_sql = 'SELECT * FROM "$aws/things/MyIotThing/*"' iot_topic_rule = iot.CfnTopicRule( self, "IoTRule", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( sql=iot_topic_rule_sql, actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=lambdaFn.function_arn)) ])) # Lambda Resource Policy allows invocation from IoT Rule lambdaFn.add_permission( "GrantIoTRule", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_topic_rule.attr_arn)
def __init__( self, scope: core.Construct, id: str, host_name, cert_arn, zone_id, admin_user: str, admin_password: str, cloud9_instance_size: str, participant_limit: str, **kwargs, ) -> None: super().__init__(scope, id, **kwargs) stack = core.Stack.of(self) stack.template_options.description = "Connected Drink Dispenser Workshop" # Static Website props: StaticSiteProps = StaticSiteProps( fqdn=host_name, hosted_zone_id=zone_id, certificate_arn=cert_arn, error_configuration=[ { "error_code": 403, "error_caching_min_ttl": 300, "response_code": 200, "response_page_path": "/index.html", }, { "error_code": 404, "error_caching_min_ttl": 300, "response_code": 200, "response_page_path": "/index.html", }, ], output_name="CDDWebSite", ) cdd_site = StaticSiteConstruct(self, "StaticSite", props) # Custom resource to clean out static website bucket prior to delete # TODO: Move this to the StaticSiteConstruct as option props: CustomResourceProps = CustomResourceProps( name=id + "-CR-S3DeleteObjects", lambda_directory="./lambda_functions/cr_s3_delete", handler="index.main", timeout=30, runtime=lambda_.Runtime.PYTHON_3_7, environment={"BUCKET_NAME": cdd_site.bucket_name}, ) s3_delete_cr = CustomResourceConstruct(self, "EmptyCddS3Bucket", props) # DependsOn the bucket (we need to delete objects before the bucket is deleted) s3_delete_cr.resource.node.add_dependency(cdd_site.bucket_resource) policy_statement = iam.PolicyStatement() policy_statement.add_actions("s3:GetBucket*") policy_statement.add_actions("s3:GetObject*") policy_statement.add_actions("s3:DeleteObject*") policy_statement.add_actions("s3:List*") policy_statement.add_resources(cdd_site.bucket_resource.bucket_arn) policy_statement.add_resources( f"{cdd_site.bucket_resource.bucket_arn}/*") s3_delete_cr.add_policy_to_role(policy_statement) # IAM Constructs user_group = iam.Group( self, "UserGroup", group_name=id + "-CDDUserGroup", managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( "ReadOnlyAccess") ], ) # DynamoDB tables user_db = dynamodb.Table( # UserId as key, user "admin" tracks next available dispenser id # No access to users, RW to Cognito Lambda self, "UserTable", table_name=id + "-UserTable", partition_key={ "name": "userName", "type": dynamodb.AttributeType.STRING }, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY, ) dispenser_db = dynamodb.Table( # Dispenser ID and credit amount - RO to users, RW to APIs self, "DispenserTable", table_name=id + "-DispenserTable", partition_key={ "name": "dispenserId", "type": dynamodb.AttributeType.STRING, }, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY, ) dispenser_events = dynamodb.Table( # Recorded events from dispenser actions self, "DispenserEvents", table_name=id + "-DispenserEvents", partition_key={ "name": "dispenserId", "type": dynamodb.AttributeType.STRING, }, sort_key={ "name": "timestamp", "type": dynamodb.AttributeType.STRING }, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY, ) # Cognito Resources # User pool with phone_number as username props: CognitoUserPoolProps = CognitoUserPoolProps( user_pool_name=id + "-users", client_name=id + "-webclient", auto_verified_attributes=["phone_number"], schema=[ { "name": "group", "attributeDataType": "String", "mutable": True, "required": False, }, { "name": "dispenserId", "attributeDataType": "String", "mutable": True, "required": False, }, ], policies={ "passwordPolicy": { "minimumLength": 6, "requireLowercase": True, "requireNumbers": True, "requireSymbols": False, "requireUppercase": False, } }, ) user_pool = CognitoUserPoolConstruct(self, "UserPool", props) # Role and lambda triggers lambda_cognito_access_role = iam.Role( # Access to IDP calls (for triggers) self, "LambdaCognitoAccessRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement(actions=["cognito-idp:*"], resources=["*"]), iam.PolicyStatement(actions=["dynamodb:*"], resources=["*"]), ]) ], ) # Triggers for UserPool # Pre-sign-up: triggered when username, password, and phone number submitted lambda_cognito_trigger_pre_signup = lambda_.Function( self, "CogntioTriggerPreSignUp", function_name=id + "-CogntioTriggerPreSignUp", code=lambda_.AssetCode("./lambda_functions/cog_pre_signup"), handler="lambda.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_cognito_access_role, timeout=core.Duration.seconds(6), environment={ "USER_TABLE": user_db.table_name, "PARTICIPANT_LIMIT": participant_limit, }, ) lambda_cognito_trigger_pre_signup.add_permission( "AllowCognitoPreSign", principal=iam.ServicePrincipal("cognito-idp.amazonaws.com"), source_arn=user_pool.user_pool_arn, ) # Post confirmation: triggered after validation code provided lambda_cognito_trigger_post_confirm = lambda_.Function( self, "CogntioTriggerPostConfirm", function_name=id + "-CogntioTriggerPostConfirm", code=lambda_.AssetCode("./lambda_functions/cog_post_confirm"), handler="lambda.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_cognito_access_role, timeout=core.Duration.seconds(6), environment={ "USER_TABLE": user_db.table_name, "PARTICIPANT_LIMIT": participant_limit, }, ) lambda_cognito_trigger_post_confirm.add_permission( "AllowCognitoPostConfirm", principal=iam.ServicePrincipal("cognito-idp.amazonaws.com"), source_arn=user_pool.user_pool_arn, ) # Attach triggers to pool user_pool.user_pool.lambda_config = cognito.CfnUserPool.LambdaConfigProperty( pre_sign_up=lambda_cognito_trigger_pre_signup.function_arn, post_confirmation=lambda_cognito_trigger_post_confirm.function_arn, ) cognito.CfnUserPoolGroup( self, "UserPoolCDDUser", group_name="cdd_user", description="General users of CDD (participants)", user_pool_id=user_pool.user_pool_id, ) cognito.CfnUserPoolGroup( self, "UserPoolCDDAdmin", group_name="cdd_admin", description="CDD administrators", user_pool_id=user_pool.user_pool_id, ) identity_pool = cognito.CfnIdentityPool( self, "IdentityPool", identity_pool_name=id.replace("-", "") + "_idpool", allow_unauthenticated_identities=False, cognito_identity_providers=[{ "clientId": user_pool.client_id, "providerName": user_pool.provider_name, }], ) core.CfnOutput( self, "CognitoIdentityPoolId", export_name="CognitoIdentityPoolId", value=identity_pool.ref, ) # Custom resource to create admin user - cannot do via CFn to set password props: CustomResourceProps = CustomResourceProps( name=id + "-CR-CreateCognitoAdminUser", lambda_directory="./lambda_functions/cr_create_admin_user", handler="index.main", timeout=30, runtime=lambda_.Runtime.PYTHON_3_7, environment={ "COGNITO_USER_POOL_ID": user_pool.user_pool_id, "COGNITO_CLIENT_ID": user_pool.client_id, "ADMIN_USERNAME": admin_user, "ADMIN_PASSWORD": admin_password, }, ) create_admin_user_cr = CustomResourceConstruct(self, "CreateAdminUser", props) # DependsOn the user pool create_admin_user_cr.resource.node.add_dependency(user_pool) policy_statement = iam.PolicyStatement() policy_statement.add_actions("cognito-idp:SignUp") policy_statement.add_actions("cognito-idp:AdminConfirmSignUp") policy_statement.add_resources("*") create_admin_user_cr.add_policy_to_role(policy_statement) # IAM roles for identity pool auth/unauth cog_unauth_role = iam.Role( self, "cognitoUnauthRole", role_name=f"Cognito_{identity_pool.identity_pool_name}_Unauth_Role", assumed_by=iam.FederatedPrincipal( "cognito-identity.amazonaws.com", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "unauthenticated" }, }, assume_role_action="sts:AssumeRoleWithWebIdentity", ), ) cog_unauth_role.attach_inline_policy( iam.Policy( self, "cognitoUnauth", policy_name="cognitoUnauth", statements=[ iam.PolicyStatement( actions=[ "mobileanalytics:PutEvents", "cognito-sync:*" ], resources=["*"], ) ], )) cog_auth_role = iam.Role( self, "cognitoAuthRole", role_name=f"Cognito_{identity_pool.identity_pool_name}_Auth_Role", managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonESCognitoAccess") ], assumed_by=iam.FederatedPrincipal( "cognito-identity.amazonaws.com", conditions={ "StringEquals": { "cognito-identity.amazonaws.com:aud": identity_pool.ref }, "ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }, }, assume_role_action="sts:AssumeRoleWithWebIdentity", ), ) cog_auth_role.attach_inline_policy( iam.Policy( self, "cognitoAuth", policy_name="cognitoAuth", statements=[ iam.PolicyStatement( actions=[ "mobileanalytics:PutEvents", "cognito-sync:*", "execute-api:*", ], resources=["*"], ), # Provide full access to IoT for the authenticated user # The AWS IoT policy scopes down the access iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ], )) # Finally, attach auth and unauth roles to Identity pool cognito.CfnIdentityPoolRoleAttachment( self, "CDDIdentityPoolRoleAttach", identity_pool_id=identity_pool.ref, roles={ "authenticated": cog_auth_role.role_arn, "unauthenticated": cog_unauth_role.role_arn, }, ) ### Supporting IAM Roles and Policies lambda_full_access_role = iam.Role( # Wide open role for Lambda's to access other services self, "LambdaFullAccessRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement(actions=["*"], resources=["*"]), ]) ], ) lambda_iot_full_access_role = iam.Role( # Wide open role for Lambda's to access other services self, "LambdaIoTFullAccessRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement(actions=["dynamodb:*", "iot:*"], resources=["*"]), ]) ], ) lambda_api_app_role = iam.Role( # Role for APIG Lambda functions - make specific per Lambda/method if needed self, "ApiAppRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement( actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_db.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_events.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}", ], ), iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ]) ], ) lambda_api_delete_user_role = iam.Role( # Role for APIG Lambda delete user - specific as this has to delete multiple services self, "ApiDeleteUserRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement( actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_db.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_events.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}", ], ), iam.PolicyStatement(actions=["cloud9:DeleteEnvironment"], resources=["*"]), iam.PolicyStatement( actions=[ "iam:DeleteLoginProfile", "iam:ListGroupsForUser", "iam:RemoveUserFromGroup", "iam:DeleteUser", ], resources=["*"], ), iam.PolicyStatement( actions=["cognito-idp:AdminDeleteUser"], resources=["*"]), iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ]) ], ) lambda_api_dispense_role = iam.Role( # Role for lambda self, "CommandRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), inline_policies=[ iam.PolicyDocument(statements=[ iam.PolicyStatement( actions=[ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], resources=["arn:aws:logs:*:*:*"], ), iam.PolicyStatement( actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_db.table_name}", f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{dispenser_events.table_name}", ], ), iam.PolicyStatement(actions=["iot:*"], resources=["*"]), ]) ], ) # IoT Policies iot_policy_dispenser_limited = iot.CfnPolicy( self, "IoTDispenserLimitedPolicy", policy_name=id + "-DispenserLimitedAccess", policy_document={ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["iot:Connect"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:client/${{iot:Connection.Thing.ThingName}}" ], "Condition": { "Bool": { "iot:Connection.Thing.IsAttached": [True] } }, }, { "Effect": "Allow", "Action": ["iot:Receive"], "Resource": ["*"] }, { "Effect": "Allow", "Action": ["iot:Subscribe"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/$aws/things/${{iot:Certificate.Subject.CommonName}}/shadow/*", f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/$aws/things/${{iot:Certificate.Subject.CommonName}}/cmd/${{iot:Certificate.Subject.CommonName}}", ], }, { "Effect": "Allow", "Action": ["iot:Publish"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:topic/$aws/things/${{iot:Certificate.Subject.CommonName}}/shadow/update", f"arn:aws:iot:{stack.region}:{stack.account}:topic/$aws/things/${{iot:Certificate.Subject.CommonName}}/shadow/get", f"arn:aws:iot:{stack.region}:{stack.account}:topic/test/${{iot:Certificate.Subject.CommonName}}", f"arn:aws:iot:{stack.region}:{stack.account}:topic/cmd/${{iot:Certificate.Subject.CommonName}}/response", ], }, ], }, ) iot_policy_client = iot.CfnPolicy( self, "IoTClientPolicy", policy_name=id + "-IoTClientAccess", policy_document={ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["iot:Connect", "iot:Receive"], "Resource": ["*"], }, { "Effect": "Allow", "Action": ["iot:Subscribe"], "Resource": [ f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/events/*", f"arn:aws:iot:{stack.region}:{stack.account}:topicfilter/$aws/things/*/shadow/update/accepted", ], }, ], }, ) ### Lambda Functions # General Lambda Functions NOT associated with APIG lambda_process_events = lambda_.Function( self, "ProcessEvents", function_name=id + "-ProcessEvents", code=lambda_.AssetCode("./lambda_functions/process_events"), handler="process_events.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_iot_full_access_role, timeout=core.Duration.seconds(20), environment={ "EVENT_TABLE": dispenser_events.table_name, "STATUS_TABLE": dispenser_db.table_name, }, ) ## API Lambda functions # Return credit for dispenser api_credit_dispenser_function = lambda_.Function( self, "ApiCreditDispenserFunction", function_name=id + "-ApiCreditDispenserFunction", code=lambda_.AssetCode("./lambda_functions/api_credit_dispenser"), handler="credit_dispenser.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_app_role, timeout=core.Duration.seconds(15), memory_size=128, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, }, ) # Command api_command_function = lambda_.Function( self, "ApiCommandFunction", function_name=id + "-ApiCommandFunction", code=lambda_.AssetCode("./lambda_functions/api_command"), handler="command.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_app_role, timeout=core.Duration.seconds(15), memory_size=128, environment={ "DispenserTable": dispenser_db.table_name, "EventTable": dispenser_events.table_name, }, ) # Request dispense operation (set shadow or command to dispense) api_dispense_function = lambda_.Function( self, "ApiDispenseFunction", function_name=id + "-ApiDispenseFunction", code=lambda_.AssetCode("./lambda_functions/api_dispense"), handler="dispense.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_dispense_role, timeout=core.Duration.seconds(15), memory_size=128, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, }, ) # Request dispense operation (set shadow or command to dispense) api_dispenser_status_function = lambda_.Function( self, "ApiDispenserStatusFunction", function_name=id + "-ApiDispenserStatusFunction", code=lambda_.AssetCode("./lambda_functions/api_dispenser_status"), handler="dispenser_status.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_app_role, timeout=core.Duration.seconds(15), memory_size=128, environment={"DISPENSER_TABLE": dispenser_db.table_name}, ) # Request user details from user table, create resources if needed # NOTE: This uses an overley permissive policy to create the resources needed api_get_resources_function = lambda_.Function( self, "ApiGetResourcesFunction", function_name=id + "-ApiGetResourcesFunction", code=lambda_.AssetCode("./lambda_functions/api_get_resources"), handler="get_resources.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_full_access_role, # Timeout is for user creation: certain tasks such as Cloud9 may take longer timeout=core.Duration.seconds(300), memory_size=128, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, "USER_TABLE": user_db.table_name, "USER_PERMISSIONS_GROUP": user_group.group_name, "IOT_POLICY_DISPENSER_LIMITED": iot_policy_dispenser_limited.policy_name, "IOT_POLICY_CLIENT": iot_policy_client.policy_name, "CLOUD9_INSTANCE_SIZE": cloud9_instance_size, }, ) # Request user details from user table api_delete_user_function = lambda_.Function( self, "ApiDeleteUserFunction", function_name=id + "-ApiDeleteUserFunction", code=lambda_.AssetCode("./lambda_functions/api_delete_user"), handler="delete_user.handler", runtime=lambda_.Runtime.PYTHON_3_7, role=lambda_api_delete_user_role, timeout=core.Duration.seconds(28), memory_size=256, environment={ "DISPENSER_TABLE": dispenser_db.table_name, "EVENT_TABLE": dispenser_events.table_name, "USER_TABLE": user_db.table_name, "USER_POOL_ID": user_pool.user_pool_id, }, ) ### API Gateway api = apigateway.RestApi( self, id + "-API", api_key_source_type=apigateway.ApiKeySourceType.HEADER, deploy_options=apigateway.StageOptions(stage_name="prod"), ) core.CfnOutput( self, "APIEndpoint", export_name="APIEndpoint", value= f"https://{api.rest_api_id}.execute-api.{stack.region}.amazonaws.com/prod/", ) # Although / is not used as method, provide OPTIONS for hinting CORS add_cors_options(api.root) # Define Cognito authorizer and attach to gateway cog_authorizer = apigateway.CfnAuthorizer( self, "CognitoAuth", name="CognitoAuthName", rest_api_id=api.rest_api_id, type="COGNITO_USER_POOLS", identity_source="method.request.header.Authorization", provider_arns=[user_pool.user_pool_arn], ) # # Resources (paths) and methods (GET, POST, etc.), for the API api_credit_resource = api.root.add_resource("credit") add_resource_method( api_credit_resource, http_method="GET", integration=apigateway.LambdaIntegration( api_credit_dispenser_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_credit_resource) # command api_command_resource = api.root.add_resource("command") add_resource_method( api_command_resource, http_method="GET", integration=apigateway.LambdaIntegration(api_command_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_command_resource) # Actuate dispenser api_dispense_resource = api.root.add_resource("dispense") add_resource_method( api_dispense_resource, http_method="GET", integration=apigateway.LambdaIntegration(api_dispense_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_dispense_resource) # Return dispenser status (from DynamoDB) api_dispenser_status_resource = api.root.add_resource("status") add_resource_method( api_dispenser_status_resource, http_method="GET", integration=apigateway.LambdaIntegration( api_dispenser_status_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_dispenser_status_resource) # Return user details from User Table api_get_resources_resource = api.root.add_resource("getResources") add_resource_method( api_get_resources_resource, http_method="POST", integration=apigateway.LambdaIntegration( api_get_resources_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_get_resources_resource) # Create a user based on valid token api_delete_user_resource = api.root.add_resource("deleteUser") add_resource_method( api_delete_user_resource, http_method="POST", integration=apigateway.LambdaIntegration(api_delete_user_function), authorization_type=apigateway.AuthorizationType.COGNITO, authorizer=cog_authorizer, ) add_cors_options(api_delete_user_resource) # Create policy and reference group iam.Policy( self, "UserPermissionsPolicy", groups=[user_group], policy_name=id + "-UserPermissions", statements=[ # Elevated permissions beyond the ReadOnlyUser # Allow seeing all MQTT messages iam.PolicyStatement( actions=["iot:Subscribe", "iot:Connect", "iot:Receive"], resources=["*"], ), # Allow search indexing iam.PolicyStatement( actions=["iot:SearchIndex"], resources=[ f"arn:aws:iot:{stack.region}:{stack.account}:index/AWS_Things" ], ), # Allow changing of security group ingress on EC2 (Cloud9) to support mapping 443 to iam.PolicyStatement( actions=[ "ec2:AuthorizeSecurityGroupIngress", "ec2:RevokeSecurityGroupIngress", ], resources=[ f"arn:aws:ec2:{stack.region}:{stack.account}:security-group/*" ], ), # DENY access to credentials table iam.PolicyStatement( effect=iam.Effect.DENY, actions=["dynamodb:*"], resources=[ f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}" ], ), # DENY access to S3 overall iam.PolicyStatement(effect=iam.Effect.DENY, actions=["s3:*"], resources=["*"]), ], ) # IoT Constructs # Rule to process shadow events and send to logging iot_rule_log_shadow_events = iot.CfnTopicRule( self, "LogShadowEventsRule", rule_name=id.replace("-", "") + "_LogShadowEvents", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description= "Based on shadow topic and content, process messages via Lambda", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, topic() AS topic FROM '$aws/things/+/shadow/update/documents'", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=lambda_process_events.function_arn)) ], ), ) # Allow rule to invoke the logging function lambda_process_events.add_permission( "AllowIoTRule1", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_log_shadow_events.attr_arn, ) # Rule to process generic events and send to logging iot_rule_log_generic_events = iot.CfnTopicRule( self, "LogGenericEventsRule", rule_name=id.replace("-", "") + "_LogGenericEvents", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description="Log generic events, enrich, then send to Lambda", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, timestamp() AS ts, topic() AS topic FROM 'events'", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=lambda_process_events.function_arn)) ], ), ) # Allow generic_events rule to Invoke the process_events function lambda_process_events.add_permission( "AllowIoTRule2", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_log_generic_events.attr_arn, ) # Rule to process dispenser specific events and send to logging iot_rule_log_dispenser_events = iot.CfnTopicRule( self, "LogDispenserEventsRule", rule_name=id.replace("-", "") + "_LogDispenserEvents", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description= "Log specific dispenser events, enrich, then send to Lambda", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, timestamp() AS ts, topic() AS topic FROM 'events/+'", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=lambda_process_events.function_arn)) ], ), ) # Allow log_dispenser_events rule to Invoke the process_events function lambda_process_events.add_permission( "AllowIoTRule3", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_log_dispenser_events.attr_arn, ) # Rule to process cmd/NNN/response WHERE "command=dispense" iot_rule_command_response_dispense = iot.CfnTopicRule( self, "DispenseCommandResponseRule", rule_name=id.replace("-", "") + "_DispenseCommandResponse", topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( description= "Invoke Lambda to process dispense commands from dispenser", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql= "select *, topic() AS topic FROM '$aws/things/+/shadow/update/accepted' WHERE isUndefined(state.reported.response) = False", actions=[ iot.CfnTopicRule.ActionProperty( lambda_=iot.CfnTopicRule.LambdaActionProperty( function_arn=api_dispense_function.function_arn)) ], ), ) # Allow command_response rule to Invoke the dispense function to reconcile outstanding requests api_dispense_function.add_permission( "AllowIoTCommandResponseRule", principal=iam.ServicePrincipal("iot.amazonaws.com"), source_arn=iot_rule_command_response_dispense.attr_arn, ) # Custom resource to delete workshop users - run to clean up any lingering ones # if the admin user didn't clean up. A lot of dependsOn as users are created with bindings # to other resources props: CustomResourceProps = CustomResourceProps( name=id + "-CR-DeleteParticipantUsers", lambda_directory="./lambda_functions/cr_delete_participant_users", handler="index.main", timeout=30, runtime=lambda_.Runtime.PYTHON_3_7, environment={ # Read user records from UserTable "USER_TABLE": user_db.table_name, # Invoke the api_delete_user function "DELETE_USER_LAMBDA_FUNCTION": api_delete_user_function.function_arn, }, ) delete_participant_users_cr = CustomResourceConstruct( self, "DeleteParticpantUsers", props) # DependsOn the API Delete User Function delete_participant_users_cr.resource.node.add_dependency( api_delete_user_function) # DependsOn the user pool to delete Cognito users delete_participant_users_cr.resource.node.add_dependency(user_pool) # DependsOn the DynamoDB UserTable delete_participant_users_cr.resource.node.add_dependency(user_db) # DependsOn the IoT dispenser and client policies delete_participant_users_cr.resource.node.add_dependency( iot_policy_dispenser_limited) delete_participant_users_cr.resource.node.add_dependency( iot_policy_client) # DependsOn the IoT IAM user group delete_participant_users_cr.resource.node.add_dependency(user_group) # Permissions for function to delete users policy_statement = iam.PolicyStatement() policy_statement.add_actions("dynamodb:*") policy_statement.add_resources( f"arn:aws:dynamodb:{stack.region}:{stack.account}:table/{user_db.table_name}" ) delete_participant_users_cr.add_policy_to_role(policy_statement) policy_statement = iam.PolicyStatement() policy_statement.add_actions("lambda:InvokeFunction") policy_statement.add_resources(api_delete_user_function.function_arn) delete_participant_users_cr.add_policy_to_role(policy_statement)
def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) #################################################################################### # IoT Events # IoT Events: Execution role iot_events_execution_role = iam.Role( self, "IoTEventsExecutionRole", assumed_by=iam.ServicePrincipal("iotevents.amazonaws.com")) iot_events_execution_role.add_to_policy( iam.PolicyStatement(resources=[ "arn:aws:iot:" + Aws.REGION + ":" + Aws.ACCOUNT_ID + ":topic/awsiotcorelorawan/*" ], actions=["iot:Publish"])) # IoT Events: Input inputDefinitionProperty = iotevents.CfnInput.InputDefinitionProperty( attributes=[{ "jsonPath": "deviceid" }, { "jsonPath": "timestamp_ms" }]) iot_events_input = iotevents.CfnInput( self, "LoRaWANDeviceWatchdogInput", input_definition=inputDefinitionProperty, input_name="LoRaWANDeviceWatchdogInput", input_description= "Input for connectivity status updates for LoRaWAN devices") # IoT Events: Detector Model notify_if_inactive_seconds_value = cdk.CfnParameter( self, "notifyifinactivseconds", type="Number").value_as_number lorawan_device_heartbeat_detectormodel_definition = iotevents.CfnDetectorModel.DetectorModelDefinitionProperty( initial_state_name=lorawan_device_heartbeat_detectormodel. initial_state_name, states=lorawan_device_heartbeat_detectormodel.get_states( self, notify_if_inactive_seconds=notify_if_inactive_seconds_value)) iot_events_model_1 = iotevents.CfnDetectorModel( self, "LoRaWANDeviceHeartbeatWatchdogModel", detector_model_definition= lorawan_device_heartbeat_detectormodel_definition, detector_model_name="LoRaWANDeviceHeartbeatWatchdogModel", detector_model_description= "Detector model for LoRaWAN device heartbeat", key="deviceid", evaluation_method="BATCH", role_arn=iot_events_execution_role.role_arn) #################################################################################### # AWS IoT TRule iot_rule_role = iam.Role( self, "LoRaWANDeviceHeartbeatWatchdogSampleRuleRole", assumed_by=iam.ServicePrincipal("iot.amazonaws.com")) iot_rule_role.add_to_policy( iam.PolicyStatement(resources=[ "arn:aws:iotevents:" + Aws.REGION + ":" + Aws.ACCOUNT_ID + ":input/" + iot_events_input.input_name ], actions=["iotevents:BatchPutMessage"])) iot_rule_role.add_to_policy( iam.PolicyStatement(resources=[ "arn:aws:iot:" + Aws.REGION + ":" + Aws.ACCOUNT_ID + ":topic/*" ], actions=["iot:Publish"])) iot_rule = iot.CfnTopicRule( self, "LoRaWANDeviceHeartbeatWatchdogSampleRule", rule_name="LoRaWANDeviceHeartbeatWatchdogSampleRule", topic_rule_payload={ "sql": "SELECT WirelessDeviceId as deviceid, timestamp() as timestamp_ms from 'LoRaWANDeviceHeartbeatWatchdogSampleRule_sampletopic'", "actions": [{ "iotEvents": { "inputName": iot_events_input.input_name, "roleArn": iot_rule_role.role_arn } }, { "republish": { "roleArn": iot_rule_role.role_arn, "topic": "awsiotcorelorawan/debug" } }], "errorAction": { "republish": { "roleArn": iot_rule_role.role_arn, "topic": "awsiotcorelorawan/error" } }, "ruleDisabled": False, "awsIotSqlVersion": "2016-03-23", }) #################################################################################### # SNS topic sns_topic = sns.Topic( self, "LoRaWANDeviceNotificationTopic", display_name= "Topic to use for notifications about LoRaWAN device events", topic_name="LoRaWANDeviceNotificationTopic") email_address = cdk.CfnParameter(self, "emailforalarms") sns_topic.add_subscription( subscriptions.EmailSubscription(email_address.value_as_string))
def __init__(self, scope: core.App, id: str, props, **kwargs) -> None: super().__init__(scope, id, **kwargs) subnets = [] subnets.append(aws_ec2.SubnetConfiguration(name="DeviceSubnet", subnet_type = aws_ec2.SubnetType.PUBLIC, cidr_mask = 24) ) vpc = aws_ec2.Vpc(self, "DeviceVpc", max_azs=2, subnet_configuration=subnets ) # Iterate the private subnets selection = vpc.select_subnets( subnet_type=aws_ec2.SubnetType.PUBLIC ) sg = aws_ec2.SecurityGroup(self, id="FarGateSecGroup", vpc=vpc, allow_all_outbound=True, description="Allow access to virtual device", security_group_name="Virtual Device Security Group") sg.add_ingress_rule( peer=aws_ec2.Peer.any_ipv4(), connection=aws_ec2.Port.tcp(80) ) # pipeline requires versioned bucket bucket = aws_s3.Bucket( self, "SourceBucket", bucket_name="{}-{}-{}".format(props['namespace'].lower(), core.Aws.ACCOUNT_ID, core.Aws.REGION), versioned=True, removal_policy=core.RemovalPolicy.DESTROY) # ssm parameter to get bucket name later bucket_param = aws_ssm.StringParameter( self, "ParameterBucketName", parameter_name=f"{props['namespace']}-bucket", string_value=bucket.bucket_name, description='IoT playground pipeline bucket' ) # ecr repo to push docker container into ecr = aws_ecr.Repository( self, "ECR", repository_name=f"{props['namespace']}", removal_policy=core.RemovalPolicy.DESTROY ) # codebuild project meant to run in pipeline cb_docker_build = aws_codebuild.PipelineProject( self, "DockerBuild", project_name=f"{props['namespace']}-Docker-Build", build_spec=aws_codebuild.BuildSpec.from_source_filename( filename='virtual-device/docker-build-buildspec.yml'), environment=aws_codebuild.BuildEnvironment( privileged=True, ), # pass the ecr repo uri into the codebuild project so codebuild knows where to push environment_variables={ 'ecr': aws_codebuild.BuildEnvironmentVariable( value=ecr.repository_uri), 'tag': aws_codebuild.BuildEnvironmentVariable( value='virtual_device') }, description='Pipeline for CodeBuild', timeout=core.Duration.minutes(10), ) # codebuild iam permissions to read write s3 bucket.grant_read_write(cb_docker_build) # codebuild permissions to interact with ecr ecr.grant_pull_push(cb_docker_build) ecs_cluster = aws_ecs.Cluster( self, 'DeviceCluster', vpc=vpc ) fargate_task_def = aws_ecs.FargateTaskDefinition(self, 'DeviceTaskDef', cpu=512, memory_limit_mib=1024 #network_mode=aws_ecs.NetworkMode.AWS_VPC, ) # fargate_task_def.add_to_task_role_policy(aws_iam.PolicyStatement( # effect=aws_iam.Effect.ALLOW, # actions=[ # "s3:PutObject"], # resources=["*"] # )) fargate_task_def.add_to_execution_role_policy(aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=[ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents"], resources=["*"] )) container_image = aws_ecs.EcrImage( repository=ecr, tag="virtual_device" ) logging = aws_ecs.AwsLogDriver( stream_prefix="virtual_device" ) container = fargate_task_def.add_container("DeviceContainer", image=container_image, cpu=512, memory_limit_mib=1024, logging=logging, essential=True ) port_mapping = aws_ecs.PortMapping( container_port=80, host_port=80, protocol=aws_ecs.Protocol.TCP ) container.add_port_mappings(port_mapping) # The code that defines your stack goes here table = aws_dynamodb.Table(self, "DeviceFactoryCatalog", partition_key=aws_dynamodb.Attribute(name="id", type=aws_dynamodb.AttributeType.STRING), billing_mode=aws_dynamodb.BillingMode.PAY_PER_REQUEST) function = aws_lambda.Function(self, "DeviceFactoryLambda", runtime=aws_lambda.Runtime.PYTHON_3_7, handler="lambda_function.lambda_handler", code=aws_lambda.Code.asset("../lambdas/device_factory_lambda"), timeout=Duration.minutes(1)) function.add_environment("BUCKET_NAME", bucket.bucket_name) function.add_environment("ECS_CLUSTER", ecs_cluster.cluster_name) function.add_environment("ECS_TASK_DEF", fargate_task_def.task_definition_arn) function.add_environment("DDB_TABLE_DEVICE_CATALOG", table.table_name) function.add_environment("SUBNET_1", selection.subnets[0].subnet_id) function.add_environment("SUBNET_2", selection.subnets[1].subnet_id) function.add_environment("SEC_GROUP", sg.security_group_id) table.grant_read_write_data(function) function.add_to_role_policy(aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=["iot:*"], resources=["*"] )) function.add_to_role_policy(aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=["s3:PutObject", "s3:GetObject"], resources=["{}/*".format(bucket.bucket_arn)] )) function.add_to_role_policy(aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=["iam:PassRole"], resources=["arn:aws:iam::*:role/*"] )) function.add_to_role_policy(aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=["ecs:RunTask", "ecs:StopTask"], resources=["*"] )) api_gtw = aws_apigateway.LambdaRestApi(self, id="DeviceFactoryApi", rest_api_name="DeviceFactoryApi", handler=function) function_dev_cmd = aws_lambda.Function(self, "DeviceCommandLambda", runtime=aws_lambda.Runtime.PYTHON_3_7, handler="lambda_function.lambda_handler", code=aws_lambda.Code.asset("../lambdas/device_command_lambda"), timeout=Duration.minutes(1)) function_dev_cmd.add_environment("DDB_TABLE_DEVICE_CATALOG", table.table_name) function_dev_cmd.add_permission("iot-lambda-perm", principal=aws_iam.ServicePrincipal('iot.amazonaws.com'), action="lambda:InvokeFunction", source_account=core.Aws.ACCOUNT_ID , source_arn="arn:aws:iot:us-east-1:086464235113:rule/iot_playground_device_cmd_to_lambda", ) table.grant_read_write_data(function_dev_cmd) function_dev_cmd.add_to_role_policy(aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=["iot:*"], resources=["*"] )) function_dev_cmd.add_to_role_policy(aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, actions=["iam:PassRole"], resources=["arn:aws:iam::*:role/*"] )) topic_rule_payload = aws_iot.CfnTopicRule.TopicRulePayloadProperty( description="Invoke Lambda", rule_disabled=False, aws_iot_sql_version="2016-03-23", sql="select *, topic() AS topic FROM 'cmd/+/event'", actions=[ aws_iot.CfnTopicRule.ActionProperty(lambda_= aws_iot.CfnTopicRule.LambdaActionProperty( function_arn=function_dev_cmd.function_arn)), ] ) aws_iot.CfnTopicRule(self, "RuleDeviceCmdToLambda", topic_rule_payload=topic_rule_payload, rule_name="iot_playground_device_cmd_to_lambda" # dash not supported ) # ssm parameter to get api endpoint later bucket_param = aws_ssm.StringParameter( self, "ParameterDeviceFactoryEndpoint", parameter_name=f"{props['namespace']}-devicefactoryendpoint", string_value=api_gtw.url, description='IoT playground device factory endpoint' ) # ssm parameter to get api endpoint later bucket_param = aws_ssm.StringParameter( self, "ParameterEcrUri", parameter_name=f"{props['namespace']}-ecruri", string_value=ecr.repository_uri, description='IoT playground ECR URI' ) # ssm parameter to get cluster name bucket_param = aws_ssm.StringParameter( self, "ParameterClusterName", parameter_name=f"{props['namespace']}-clustername", string_value=ecs_cluster.cluster_name, description='IoT playground Cluster Name' ) core.CfnOutput( self, "EcrUri", description="ECR URI", value=ecr.repository_uri, ) core.CfnOutput( self, "S3Bucket", description="S3 Bucket", value=bucket.bucket_name ) core.CfnOutput( self, "DeviceFactoryEndpoint", description="Device Factory Endpoint", value=api_gtw.url ) self.output_props = props.copy() self.output_props['bucket']= bucket self.output_props['cb_docker_build'] = cb_docker_build