From 40aa73a12b2e964b3acd278dd7035b8e4e1bbd28 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Thu, 7 Nov 2019 17:11:13 +0000 Subject: [PATCH 1/7] #2546 - AWS Lambda: Add Role validation when creating functions --- moto/awslambda/models.py | 24 +++ moto/awslambda/responses.py | 6 +- tests/test_awslambda/test_lambda.py | 143 +++++++++++++----- .../test_lambda_eventsourcemappings.py | 0 .../test_cloudformation_stack_crud.py | 13 +- .../test_cloudformation_stack_integration.py | 13 +- 6 files changed, 149 insertions(+), 50 deletions(-) delete mode 100644 tests/test_awslambda/test_lambda_eventsourcemappings.py diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index a58582599..2f00fa94e 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -26,6 +26,8 @@ import requests.adapters import boto.awslambda from moto.core import BaseBackend, BaseModel from moto.core.exceptions import RESTError +from moto.iam.models import iam_backend +from moto.iam.exceptions import IAMNotFoundException from moto.core.utils import unix_time_millis from moto.s3.models import s3_backend from moto.logs.models import logs_backends @@ -667,6 +669,28 @@ class LambdaStorage(object): :param fn: Function :type fn: LambdaFunction """ + pattern = r"arn:(aws[a-zA-Z-]*)?:iam::(\d{12}):role/?[a-zA-Z_0-9+=,.@\-_/]+" + valid_role = re.match(pattern, fn.role) + if valid_role: + account = valid_role.group(2) + if account != ACCOUNT_ID: + raise ValueError( + "AccessDeniedException", "Cross-account pass role is not allowed." + ) + try: + iam_backend.get_role_by_arn(fn.role) + except IAMNotFoundException: + raise ValueError( + "InvalidParameterValueException", + "The role defined for the function cannot be assumed by Lambda.", + ) + else: + raise ValueError( + "ValidationException", + "1 validation error detected: Value '{0}' at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: {1}".format( + fn.role, pattern + ), + ) if fn.function_name in self._functions: self._functions[fn.function_name]["latest"] = fn else: diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 62265b310..7734eab60 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -214,11 +214,7 @@ class LambdaResponse(BaseResponse): try: fn = self.lambda_backend.create_function(self.json_body) except ValueError as e: - return ( - 400, - {}, - json.dumps({"Error": {"Code": e.args[0], "Message": e.args[1]}}), - ) + return 400, {}, json.dumps({"__type": e.args[0], "message": e.args[1]}) else: config = fn.get_configuration() return 201, {}, json.dumps(config) diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 306deeea4..2f104f049 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -15,6 +15,7 @@ from freezegun import freeze_time from moto import ( mock_dynamodb2, mock_lambda, + mock_iam, mock_s3, mock_ec2, mock_sns, @@ -22,6 +23,7 @@ from moto import ( settings, mock_sqs, ) +from moto.sts.models import ACCOUNT_ID from nose.tools import assert_raises from botocore.exceptions import ClientError @@ -96,7 +98,7 @@ def test_invoke_requestresponse_function(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file1()}, Description="test lambda function", @@ -129,7 +131,7 @@ def test_invoke_event_function(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file1()}, Description="test lambda function", @@ -163,7 +165,7 @@ if settings.TEST_SERVER_MODE: conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file2()}, Description="test lambda function", @@ -216,7 +218,7 @@ def test_invoke_function_from_sns(): result = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -260,7 +262,7 @@ def test_create_based_on_s3_with_missing_bucket(): conn.create_function.when.called_with( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "this-bucket-does-not-exist", "S3Key": "test.zip"}, Description="test lambda function", @@ -285,7 +287,7 @@ def test_create_function_from_aws_bucket(): result = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -306,7 +308,7 @@ def test_create_function_from_aws_bucket(): _lambda_region ), "Runtime": "python2.7", - "Role": "test-iam-role", + "Role": result["Role"], "Handler": "lambda_function.lambda_handler", "CodeSha256": hashlib.sha256(zip_content).hexdigest(), "CodeSize": len(zip_content), @@ -332,7 +334,7 @@ def test_create_function_from_zipfile(): result = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": zip_content}, Description="test lambda function", @@ -353,7 +355,7 @@ def test_create_function_from_zipfile(): _lambda_region ), "Runtime": "python2.7", - "Role": "test-iam-role", + "Role": result["Role"], "Handler": "lambda_function.lambda_handler", "CodeSize": len(zip_content), "Description": "test lambda function", @@ -381,7 +383,7 @@ def test_get_function(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -412,7 +414,7 @@ def test_get_function(): result["Configuration"]["FunctionName"].should.equal("testFunction") result["Configuration"]["Handler"].should.equal("lambda_function.lambda_handler") result["Configuration"]["MemorySize"].should.equal(128) - result["Configuration"]["Role"].should.equal("test-iam-role") + result["Configuration"]["Role"].should.equal(get_role_name()) result["Configuration"]["Runtime"].should.equal("python2.7") result["Configuration"]["Timeout"].should.equal(3) result["Configuration"]["Version"].should.equal("$LATEST") @@ -449,7 +451,7 @@ def test_get_function_by_arn(): fnc = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": bucket_name, "S3Key": "test.zip"}, Description="test lambda function", @@ -475,7 +477,7 @@ def test_delete_function(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -510,7 +512,7 @@ def test_delete_function_by_arn(): fnc = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": bucket_name, "S3Key": "test.zip"}, Description="test lambda function", @@ -545,7 +547,7 @@ def test_publish(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -597,7 +599,7 @@ def test_list_create_list_get_delete_list(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -622,7 +624,7 @@ def test_list_create_list_get_delete_list(): "FunctionName": "testFunction", "Handler": "lambda_function.lambda_handler", "MemorySize": 128, - "Role": "test-iam-role", + "Role": get_role_name(), "Runtime": "python2.7", "Timeout": 3, "Version": "$LATEST", @@ -663,7 +665,7 @@ def lambda_handler(event, context): client.create_function( FunctionName="test-lambda-fx", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Description="test lambda function", Timeout=3, @@ -696,7 +698,7 @@ def test_tags(): function = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -764,7 +766,7 @@ def test_invoke_async_function(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file1()}, Description="test lambda function", @@ -788,7 +790,7 @@ def test_get_function_created_with_zipfile(): result = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.handler", Code={"ZipFile": zip_content}, Description="test lambda function", @@ -817,7 +819,7 @@ def test_get_function_created_with_zipfile(): "FunctionName": "testFunction", "Handler": "lambda_function.handler", "MemorySize": 128, - "Role": "test-iam-role", + "Role": get_role_name(), "Runtime": "python2.7", "Timeout": 3, "Version": "$LATEST", @@ -833,7 +835,7 @@ def test_add_function_permission(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=(get_role_name()), Handler="lambda_function.handler", Code={"ZipFile": zip_content}, Description="test lambda function", @@ -864,7 +866,7 @@ def test_get_function_policy(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.handler", Code={"ZipFile": zip_content}, Description="test lambda function", @@ -904,7 +906,7 @@ def test_list_versions_by_function(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="arn:aws:iam::123456789012:role/test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -933,7 +935,7 @@ def test_list_versions_by_function(): conn.create_function( FunctionName="testFunction_2", Runtime="python2.7", - Role="arn:aws:iam::123456789012:role/test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -962,7 +964,7 @@ def test_create_function_with_already_exists(): conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -974,7 +976,7 @@ def test_create_function_with_already_exists(): response = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -1006,7 +1008,7 @@ def test_create_event_source_mapping(): func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -1036,7 +1038,7 @@ def test_invoke_function_from_sqs(): func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -1096,7 +1098,7 @@ def test_invoke_function_from_dynamodb(): func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function executed after a DynamoDB table is updated", @@ -1147,7 +1149,7 @@ def test_invoke_function_from_sqs_exception(): func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file4()}, Description="test lambda function", @@ -1206,7 +1208,7 @@ def test_list_event_source_mappings(): func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -1238,7 +1240,7 @@ def test_get_event_source_mapping(): func = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -1268,7 +1270,7 @@ def test_update_event_source_mapping(): func1 = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -1279,7 +1281,7 @@ def test_update_event_source_mapping(): func2 = conn.create_function( FunctionName="testFunction2", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -1312,7 +1314,7 @@ def test_delete_event_source_mapping(): func1 = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": get_test_zip_file3()}, Description="test lambda function", @@ -1348,7 +1350,7 @@ def test_update_configuration(): fxn = conn.create_function( FunctionName="testFunction", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -1393,7 +1395,7 @@ def test_update_function_zip(): fxn = conn.create_function( FunctionName="testFunctionZip", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"ZipFile": zip_content_one}, Description="test lambda function", @@ -1428,7 +1430,7 @@ def test_update_function_zip(): "FunctionName": "testFunctionZip", "Handler": "lambda_function.lambda_handler", "MemorySize": 128, - "Role": "test-iam-role", + "Role": fxn["Role"], "Runtime": "python2.7", "Timeout": 3, "Version": "2", @@ -1451,7 +1453,7 @@ def test_update_function_s3(): fxn = conn.create_function( FunctionName="testFunctionS3", Runtime="python2.7", - Role="test-iam-role", + Role=get_role_name(), Handler="lambda_function.lambda_handler", Code={"S3Bucket": "test-bucket", "S3Key": "test.zip"}, Description="test lambda function", @@ -1490,10 +1492,67 @@ def test_update_function_s3(): "FunctionName": "testFunctionS3", "Handler": "lambda_function.lambda_handler", "MemorySize": 128, - "Role": "test-iam-role", + "Role": fxn["Role"], "Runtime": "python2.7", "Timeout": 3, "Version": "2", "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []}, } ) + + +@mock_lambda +def test_create_function_with_invalid_arn(): + err = create_invalid_lambda("test-iam-role") + err.exception.response["Error"]["Code"].should.equal("ValidationException") + err.exception.response["Error"]["Message"].should.equal( + "1 validation error detected: Value 'test-iam-role' at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: arn:(aws[a-zA-Z-]*)?:iam::(\d{12}):role/?[a-zA-Z_0-9+=,.@\-_/]+" + ) + + +@mock_lambda +def test_create_function_with_arn_from_different_account(): + err = create_invalid_lambda("arn:aws:iam::000000000000:role/example_role") + err.exception.response["Error"]["Code"].should.equal("AccessDeniedException") + err.exception.response["Error"]["Message"].should.equal( + "Cross-account pass role is not allowed." + ) + + +@mock_lambda +def test_create_function_with_unknown_arn(): + err = create_invalid_lambda( + "arn:aws:iam::" + str(ACCOUNT_ID) + ":role/service-role/unknown_role" + ) + err.exception.response["Error"]["Code"].should.equal( + "InvalidParameterValueException" + ) + err.exception.response["Error"]["Message"].should.equal( + "The role defined for the function cannot be assumed by Lambda." + ) + + +def create_invalid_lambda(role): + conn = boto3.client("lambda", "us-west-2") + zip_content = get_test_zip_file1() + with assert_raises(ClientError) as err: + conn.create_function( + FunctionName="testFunction", + Runtime="python2.7", + Role=role, + Handler="lambda_function.handler", + Code={"ZipFile": zip_content}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + return err + + +def get_role_name(): + with mock_iam(): + iam = boto3.client("iam") + return iam.create_role( + RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/" + )["Role"]["Arn"] diff --git a/tests/test_awslambda/test_lambda_eventsourcemappings.py b/tests/test_awslambda/test_lambda_eventsourcemappings.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index bfd66935a..efd4eb0fd 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -4,6 +4,7 @@ import os import json import boto +import boto.iam import boto.s3 import boto.s3.key import boto.cloudformation @@ -20,6 +21,7 @@ from moto import ( mock_route53_deprecated, ) from moto.cloudformation import cloudformation_backends +from moto.iam import mock_iam_deprecated dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -516,7 +518,7 @@ def test_create_stack_lambda_and_dynamodb(): "Code": {"S3Bucket": "bucket_123", "S3Key": "key_123"}, "FunctionName": "func1", "Handler": "handler.handler", - "Role": "role1", + "Role": get_role_name(), "Runtime": "python2.7", "Description": "descr", "MemorySize": 12345, @@ -591,3 +593,12 @@ def test_create_stack_kinesis(): stack = conn.describe_stacks()[0] resources = stack.list_resources() assert len(resources) == 1 + + +def get_role_name(): + with mock_iam_deprecated(): + iam = boto.connect_iam() + role = iam.create_role("my-role")["create_role_response"]["create_role_result"][ + "role" + ]["arn"] + return role diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index ced6b2005..f969acfd1 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1773,7 +1773,7 @@ def lambda_handler(event, context): "Handler": "lambda_function.handler", "Description": "Test function", "MemorySize": 128, - "Role": "test-role", + "Role": get_role_name(), "Runtime": "python2.7", "Environment": {"Variables": {"TEST_ENV_KEY": "test-env-val"}}, }, @@ -1791,7 +1791,7 @@ def lambda_handler(event, context): result["Functions"][0]["Description"].should.equal("Test function") result["Functions"][0]["Handler"].should.equal("lambda_function.handler") result["Functions"][0]["MemorySize"].should.equal(128) - result["Functions"][0]["Role"].should.equal("test-role") + result["Functions"][0]["Role"].should.equal(get_role_name()) result["Functions"][0]["Runtime"].should.equal("python2.7") result["Functions"][0]["Environment"].should.equal( {"Variables": {"TEST_ENV_KEY": "test-env-val"}} @@ -2311,3 +2311,12 @@ def test_stack_dynamodb_resources_integration(): response["Item"]["Sales"].should.equal(Decimal("10")) response["Item"]["NumberOfSongs"].should.equal(Decimal("5")) response["Item"]["Album"].should.equal("myAlbum") + + +def get_role_name(): + with mock_iam_deprecated(): + iam = boto.connect_iam() + role = iam.create_role("my-role")["create_role_response"]["create_role_result"][ + "role" + ]["arn"] + return role From ad23b6578430c2d690f12da0f4d87744e46beadb Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Thu, 7 Nov 2019 11:18:03 -0800 Subject: [PATCH 2/7] Implement CloudFormations IAMRole::GetArn --- moto/iam/models.py | 2 +- .../test_cloudformation_stack_integration.py | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index 2a76e9126..564a07afb 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -370,7 +370,7 @@ class Role(BaseModel): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException if attribute_name == "Arn": - raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') + return self.arn raise UnformattedGetAttTemplateException() def get_tags(self): diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index f969acfd1..e789f6e6b 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1773,11 +1773,25 @@ def lambda_handler(event, context): "Handler": "lambda_function.handler", "Description": "Test function", "MemorySize": 128, - "Role": get_role_name(), + "Role": {"Fn::GetAtt": ["MyRole", "Arn"]}, "Runtime": "python2.7", "Environment": {"Variables": {"TEST_ENV_KEY": "test-env-val"}}, }, - } + }, + "MyRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": ["sts:AssumeRole"], + "Effect": "Allow", + "Principal": {"Service": ["ec2.amazonaws.com"]}, + } + ] + } + }, + }, }, } @@ -1791,7 +1805,6 @@ def lambda_handler(event, context): result["Functions"][0]["Description"].should.equal("Test function") result["Functions"][0]["Handler"].should.equal("lambda_function.handler") result["Functions"][0]["MemorySize"].should.equal(128) - result["Functions"][0]["Role"].should.equal(get_role_name()) result["Functions"][0]["Runtime"].should.equal("python2.7") result["Functions"][0]["Environment"].should.equal( {"Variables": {"TEST_ENV_KEY": "test-env-val"}} @@ -2311,12 +2324,3 @@ def test_stack_dynamodb_resources_integration(): response["Item"]["Sales"].should.equal(Decimal("10")) response["Item"]["NumberOfSongs"].should.equal(Decimal("5")) response["Item"]["Album"].should.equal("myAlbum") - - -def get_role_name(): - with mock_iam_deprecated(): - iam = boto.connect_iam() - role = iam.create_role("my-role")["create_role_response"]["create_role_result"][ - "role" - ]["arn"] - return role From c6dd3c96ea8e4604ae106c8fb00e5f291c6ed4fe Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sun, 17 Nov 2019 10:59:20 +0000 Subject: [PATCH 3/7] AWSLambda - refactor exception handling --- moto/awslambda/exceptions.py | 31 +++++++++++++++++++ moto/awslambda/models.py | 29 +++++++---------- moto/awslambda/responses.py | 24 ++++---------- .../test_cloudformation_stack_crud.py | 14 ++++----- 4 files changed, 56 insertions(+), 42 deletions(-) create mode 100644 moto/awslambda/exceptions.py diff --git a/moto/awslambda/exceptions.py b/moto/awslambda/exceptions.py new file mode 100644 index 000000000..1a82977c3 --- /dev/null +++ b/moto/awslambda/exceptions.py @@ -0,0 +1,31 @@ +from botocore.client import ClientError + + +class LambdaClientError(ClientError): + def __init__(self, error, message): + error_response = {"Error": {"Code": error, "Message": message}} + super(LambdaClientError, self).__init__(error_response, None) + + +class CrossAccountNotAllowed(LambdaClientError): + def __init__(self): + super(CrossAccountNotAllowed, self).__init__( + "AccessDeniedException", "Cross-account pass role is not allowed." + ) + + +class InvalidParameterValueException(LambdaClientError): + def __init__(self, message): + super(InvalidParameterValueException, self).__init__( + "InvalidParameterValueException", message + ) + + +class InvalidRoleFormat(LambdaClientError): + pattern = r"arn:(aws[a-zA-Z-]*)?:iam::(\d{12}):role/?[a-zA-Z_0-9+=,.@\-_/]+" + + def __init__(self, role): + message = "1 validation error detected: Value '{0}' at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: {1}".format( + role, InvalidRoleFormat.pattern + ) + super(InvalidRoleFormat, self).__init__("ValidationException", message) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 2f00fa94e..48f52f4d9 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -33,6 +33,11 @@ from moto.s3.models import s3_backend from moto.logs.models import logs_backends from moto.s3.exceptions import MissingBucket, MissingKey from moto import settings +from .exceptions import ( + CrossAccountNotAllowed, + InvalidRoleFormat, + InvalidParameterValueException, +) from .utils import make_function_arn, make_function_ver_arn from moto.sqs import sqs_backends from moto.dynamodb2 import dynamodb_backends2 @@ -215,9 +220,8 @@ class LambdaFunction(BaseModel): key = s3_backend.get_key(self.code["S3Bucket"], self.code["S3Key"]) except MissingBucket: if do_validate_s3(): - raise ValueError( - "InvalidParameterValueException", - "Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist", + raise InvalidParameterValueException( + "Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist" ) except MissingKey: if do_validate_s3(): @@ -669,28 +673,19 @@ class LambdaStorage(object): :param fn: Function :type fn: LambdaFunction """ - pattern = r"arn:(aws[a-zA-Z-]*)?:iam::(\d{12}):role/?[a-zA-Z_0-9+=,.@\-_/]+" - valid_role = re.match(pattern, fn.role) + valid_role = re.match(InvalidRoleFormat.pattern, fn.role) if valid_role: account = valid_role.group(2) if account != ACCOUNT_ID: - raise ValueError( - "AccessDeniedException", "Cross-account pass role is not allowed." - ) + raise CrossAccountNotAllowed() try: iam_backend.get_role_by_arn(fn.role) except IAMNotFoundException: - raise ValueError( - "InvalidParameterValueException", - "The role defined for the function cannot be assumed by Lambda.", + raise InvalidParameterValueException( + "The role defined for the function cannot be assumed by Lambda." ) else: - raise ValueError( - "ValidationException", - "1 validation error detected: Value '{0}' at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: {1}".format( - fn.role, pattern - ), - ) + raise InvalidRoleFormat(fn.role) if fn.function_name in self._functions: self._functions[fn.function_name]["latest"] = fn else: diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 7734eab60..bef032143 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -211,26 +211,14 @@ class LambdaResponse(BaseResponse): return 200, {}, json.dumps(result) def _create_function(self, request, full_url, headers): - try: - fn = self.lambda_backend.create_function(self.json_body) - except ValueError as e: - return 400, {}, json.dumps({"__type": e.args[0], "message": e.args[1]}) - else: - config = fn.get_configuration() - return 201, {}, json.dumps(config) + fn = self.lambda_backend.create_function(self.json_body) + config = fn.get_configuration() + return 201, {}, json.dumps(config) def _create_event_source_mapping(self, request, full_url, headers): - try: - fn = self.lambda_backend.create_event_source_mapping(self.json_body) - except ValueError as e: - return ( - 400, - {}, - json.dumps({"Error": {"Code": e.args[0], "Message": e.args[1]}}), - ) - else: - config = fn.get_configuration() - return 201, {}, json.dumps(config) + fn = self.lambda_backend.create_event_source_mapping(self.json_body) + config = fn.get_configuration() + return 201, {}, json.dumps(config) def _list_event_source_mappings(self, event_source_arn, function_name): esms = self.lambda_backend.list_event_source_mappings( diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index efd4eb0fd..8ae01e7f2 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -4,6 +4,7 @@ import os import json import boto +import boto3 import boto.iam import boto.s3 import boto.s3.key @@ -19,9 +20,9 @@ from moto import ( mock_cloudformation_deprecated, mock_s3_deprecated, mock_route53_deprecated, + mock_iam, ) from moto.cloudformation import cloudformation_backends -from moto.iam import mock_iam_deprecated dummy_template = { "AWSTemplateFormatVersion": "2010-09-09", @@ -596,9 +597,8 @@ def test_create_stack_kinesis(): def get_role_name(): - with mock_iam_deprecated(): - iam = boto.connect_iam() - role = iam.create_role("my-role")["create_role_response"]["create_role_result"][ - "role" - ]["arn"] - return role + with mock_iam(): + iam = boto3.client("iam") + return iam.create_role( + RoleName="TestRole", AssumeRolePolicyDocument="some_policy" + )["Role"]["Arn"] From 1d852882448b09a4b9f5813e11ed6e78104b54ab Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Wed, 20 Nov 2019 08:27:46 +0000 Subject: [PATCH 4/7] Ensure Flask sends through custom error messages --- moto/core/utils.py | 6 +++++- tests/test_awslambda/test_lambda.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/moto/core/utils.py b/moto/core/utils.py index a15b7cd1e..57ff0f1b4 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -8,6 +8,7 @@ import random import re import six import string +from botocore.exceptions import ClientError from six.moves.urllib.parse import urlparse @@ -141,7 +142,10 @@ class convert_flask_to_httpretty_response(object): def __call__(self, args=None, **kwargs): from flask import request, Response - result = self.callback(request, request.url, {}) + try: + result = self.callback(request, request.url, {}) + except ClientError as exc: + result = 400, {}, exc.response["Error"]["Message"] # result is a status, headers, response tuple if len(result) == 3: status, headers, content = result diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index ab8130a3a..bc551db05 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1506,7 +1506,6 @@ def test_update_function_s3(): @mock_lambda def test_create_function_with_invalid_arn(): err = create_invalid_lambda("test-iam-role") - err.exception.response["Error"]["Code"].should.equal("ValidationException") err.exception.response["Error"]["Message"].should.equal( "1 validation error detected: Value 'test-iam-role' at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: arn:(aws[a-zA-Z-]*)?:iam::(\d{12}):role/?[a-zA-Z_0-9+=,.@\-_/]+" ) @@ -1515,7 +1514,6 @@ def test_create_function_with_invalid_arn(): @mock_lambda def test_create_function_with_arn_from_different_account(): err = create_invalid_lambda("arn:aws:iam::000000000000:role/example_role") - err.exception.response["Error"]["Code"].should.equal("AccessDeniedException") err.exception.response["Error"]["Message"].should.equal( "Cross-account pass role is not allowed." ) @@ -1526,9 +1524,6 @@ def test_create_function_with_unknown_arn(): err = create_invalid_lambda( "arn:aws:iam::" + str(ACCOUNT_ID) + ":role/service-role/unknown_role" ) - err.exception.response["Error"]["Code"].should.equal( - "InvalidParameterValueException" - ) err.exception.response["Error"]["Message"].should.equal( "The role defined for the function cannot be assumed by Lambda." ) @@ -1555,6 +1550,11 @@ def create_invalid_lambda(role): def get_role_name(): with mock_iam(): iam = boto3.client("iam") - return iam.create_role( - RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/" - )["Role"]["Arn"] + try: + return iam.get_role(RoleName="my-role")["Role"]["Arn"] + except ClientError: + return iam.create_role( + RoleName="my-role", + AssumeRolePolicyDocument="some policy", + Path="/my-path/", + )["Role"]["Arn"] From aa56715d838a531cac0880f73fecd8758ac416f3 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Fri, 22 Nov 2019 06:31:13 +0000 Subject: [PATCH 5/7] Linting --- moto/batch/models.py | 4 +--- moto/cloudformation/responses.py | 4 +--- moto/iam/models.py | 4 +--- moto/iam/policy_validation.py | 4 +--- tests/test_ecs/test_ecs_boto3.py | 4 +--- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/moto/batch/models.py b/moto/batch/models.py index 1a28fdcc5..ab52db54c 100644 --- a/moto/batch/models.py +++ b/moto/batch/models.py @@ -302,9 +302,7 @@ class Job(threading.Thread, BaseModel): self.job_id = str(uuid.uuid4()) self.job_definition = job_def self.job_queue = job_queue - self.job_state = ( - "SUBMITTED" - ) # One of SUBMITTED | PENDING | RUNNABLE | STARTING | RUNNING | SUCCEEDED | FAILED + self.job_state = "SUBMITTED" # One of SUBMITTED | PENDING | RUNNABLE | STARTING | RUNNING | SUCCEEDED | FAILED self.job_queue.jobs.append(self) self.job_started_at = datetime.datetime(1970, 1, 1) self.job_stopped_at = datetime.datetime(1970, 1, 1) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index fe875fed5..f5e094c15 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -177,9 +177,7 @@ class CloudFormationResponse(BaseResponse): start = stack_ids.index(token) + 1 else: start = 0 - max_results = ( - 50 - ) # using this to mske testing of paginated stacks more convenient than default 1 MB + max_results = 50 # using this to mske testing of paginated stacks more convenient than default 1 MB stacks_resp = stacks[start : start + max_results] next_token = None if len(stacks) > (start + max_results): diff --git a/moto/iam/models.py b/moto/iam/models.py index a73ee709c..c67d5b365 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -788,9 +788,7 @@ class AccountSummary(BaseModel): self._groups_per_user_quota = 10 self._attached_policies_per_user_quota = 10 self._policies_quota = 1500 - self._account_mfa_enabled = ( - 0 - ) # Haven't found any information being able to activate MFA for the root account programmatically + self._account_mfa_enabled = 0 # Haven't found any information being able to activate MFA for the root account programmatically self._access_keys_per_user_quota = 2 self._assume_role_policy_size_quota = 2048 self._policy_versions_in_use_quota = 10000 diff --git a/moto/iam/policy_validation.py b/moto/iam/policy_validation.py index c1a16cc5d..95610ac4d 100644 --- a/moto/iam/policy_validation.py +++ b/moto/iam/policy_validation.py @@ -88,9 +88,7 @@ class IAMPolicyDocumentValidator: self._policy_document = policy_document self._policy_json = {} self._statements = [] - self._resource_error = ( - "" - ) # the first resource error found that does not generate a legacy parsing error + self._resource_error = "" # the first resource error found that does not generate a legacy parsing error def validate(self): try: diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 38095bdc5..224e6935b 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -1924,9 +1924,7 @@ def test_attributes(): ) attrs = resp["attributes"] - NUM_CUSTOM_ATTRIBUTES = ( - 4 - ) # 2 specific to individual machines and 1 global, going to both machines (2 + 1*2) + NUM_CUSTOM_ATTRIBUTES = 4 # 2 specific to individual machines and 1 global, going to both machines (2 + 1*2) NUM_DEFAULT_ATTRIBUTES = 4 len(attrs).should.equal( NUM_CUSTOM_ATTRIBUTES + (NUM_DEFAULT_ATTRIBUTES * len(instances)) From d759f163317586e478dd313154834be95077e2a8 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Fri, 22 Nov 2019 07:39:33 +0000 Subject: [PATCH 6/7] Specify region name when creating Lambda roles --- tests/test_awslambda/test_lambda.py | 2 +- tests/test_cloudformation/test_cloudformation_stack_crud.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 6eaf1107c..e0e8a8205 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1549,7 +1549,7 @@ def create_invalid_lambda(role): def get_role_name(): with mock_iam(): - iam = boto3.client("iam") + iam = boto3.client("iam", region_name="us-west-2") try: return iam.get_role(RoleName="my-role")["Role"]["Arn"] except ClientError: diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 8ae01e7f2..c499d21bc 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -598,7 +598,7 @@ def test_create_stack_kinesis(): def get_role_name(): with mock_iam(): - iam = boto3.client("iam") + iam = boto3.client("iam", region_name="us-west-2") return iam.create_role( RoleName="TestRole", AssumeRolePolicyDocument="some_policy" )["Role"]["Arn"] From 8362179f70c5553b81c8c8cc60bc687238bb8db9 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sat, 23 Nov 2019 10:18:56 +0000 Subject: [PATCH 7/7] Use deprecated IAM, only plays nice with deprecated CF --- .../test_cloudformation_stack_crud.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index c499d21bc..3de758a02 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -4,7 +4,6 @@ import os import json import boto -import boto3 import boto.iam import boto.s3 import boto.s3.key @@ -20,7 +19,7 @@ from moto import ( mock_cloudformation_deprecated, mock_s3_deprecated, mock_route53_deprecated, - mock_iam, + mock_iam_deprecated, ) from moto.cloudformation import cloudformation_backends @@ -597,8 +596,9 @@ def test_create_stack_kinesis(): def get_role_name(): - with mock_iam(): - iam = boto3.client("iam", region_name="us-west-2") - return iam.create_role( - RoleName="TestRole", AssumeRolePolicyDocument="some_policy" - )["Role"]["Arn"] + with mock_iam_deprecated(): + iam = boto.connect_iam() + role = iam.create_role("my-role")["create_role_response"]["create_role_result"][ + "role" + ]["arn"] + return role