From af4082f38edbd78d0f56a7e8bd38340f46a96e74 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Mon, 2 Sep 2019 16:26:40 +0100 Subject: [PATCH 1/4] Step Functions - State Machines methods --- IMPLEMENTATION_COVERAGE.md | 10 +- docs/index.rst | 2 + moto/__init__.py | 1 + moto/backends.py | 2 + moto/stepfunctions/__init__.py | 6 + moto/stepfunctions/exceptions.py | 35 +++ moto/stepfunctions/models.py | 121 ++++++++ moto/stepfunctions/responses.py | 80 +++++ moto/stepfunctions/urls.py | 10 + .../test_stepfunctions/test_stepfunctions.py | 276 ++++++++++++++++++ 10 files changed, 538 insertions(+), 5 deletions(-) create mode 100644 moto/stepfunctions/__init__.py create mode 100644 moto/stepfunctions/exceptions.py create mode 100644 moto/stepfunctions/models.py create mode 100644 moto/stepfunctions/responses.py create mode 100644 moto/stepfunctions/urls.py create mode 100644 tests/test_stepfunctions/test_stepfunctions.py diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index d149b0dd8670..7a839fb96a04 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -6050,19 +6050,19 @@ ## stepfunctions 0% implemented - [ ] create_activity -- [ ] create_state_machine +- [X] create_state_machine - [ ] delete_activity -- [ ] delete_state_machine +- [X] delete_state_machine - [ ] describe_activity - [ ] describe_execution -- [ ] describe_state_machine +- [X] describe_state_machine - [ ] describe_state_machine_for_execution - [ ] get_activity_task - [ ] get_execution_history - [ ] list_activities - [ ] list_executions -- [ ] list_state_machines -- [ ] list_tags_for_resource +- [X] list_state_machines +- [X] list_tags_for_resource - [ ] send_task_failure - [ ] send_task_heartbeat - [ ] send_task_success diff --git a/docs/index.rst b/docs/index.rst index 4811fb797288..6311597fe746 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -94,6 +94,8 @@ Currently implemented Services: +---------------------------+-----------------------+------------------------------------+ | SES | @mock_ses | all endpoints done | +---------------------------+-----------------------+------------------------------------+ +| SFN | @mock_stepfunctions | basic endpoints done | ++---------------------------+-----------------------+------------------------------------+ | SNS | @mock_sns | all endpoints done | +---------------------------+-----------------------+------------------------------------+ | SQS | @mock_sqs | core endpoints done | diff --git a/moto/__init__.py b/moto/__init__.py index 8594cedd2526..f82a411cf45f 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -42,6 +42,7 @@ from .secretsmanager import mock_secretsmanager # flake8: noqa from .sns import mock_sns, mock_sns_deprecated # flake8: noqa from .sqs import mock_sqs, mock_sqs_deprecated # flake8: noqa +from .stepfunctions import mock_stepfunctions # flake8: noqa from .sts import mock_sts, mock_sts_deprecated # flake8: noqa from .ssm import mock_ssm # flake8: noqa from .route53 import mock_route53, mock_route53_deprecated # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index 6ea85093d27b..8a20697c299b 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -40,6 +40,7 @@ from moto.sns import sns_backends from moto.sqs import sqs_backends from moto.ssm import ssm_backends +from moto.stepfunctions import stepfunction_backends from moto.sts import sts_backends from moto.swf import swf_backends from moto.xray import xray_backends @@ -91,6 +92,7 @@ 'sns': sns_backends, 'sqs': sqs_backends, 'ssm': ssm_backends, + 'stepfunctions': stepfunction_backends, 'sts': sts_backends, 'swf': swf_backends, 'route53': route53_backends, diff --git a/moto/stepfunctions/__init__.py b/moto/stepfunctions/__init__.py new file mode 100644 index 000000000000..dc2b0ba13259 --- /dev/null +++ b/moto/stepfunctions/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import stepfunction_backends +from ..core.models import base_decorator + +stepfunction_backend = stepfunction_backends['us-east-1'] +mock_stepfunctions = base_decorator(stepfunction_backends) diff --git a/moto/stepfunctions/exceptions.py b/moto/stepfunctions/exceptions.py new file mode 100644 index 000000000000..a7c0897a5070 --- /dev/null +++ b/moto/stepfunctions/exceptions.py @@ -0,0 +1,35 @@ +from __future__ import unicode_literals +import json + + +class AWSError(Exception): + CODE = None + STATUS = 400 + + def __init__(self, message, code=None, status=None): + self.message = message + self.code = code if code is not None else self.CODE + self.status = status if status is not None else self.STATUS + + def response(self): + return json.dumps({'__type': self.code, 'message': self.message}), dict(status=self.status) + + +class AccessDeniedException(AWSError): + CODE = 'AccessDeniedException' + STATUS = 400 + + +class InvalidArn(AWSError): + CODE = 'InvalidArn' + STATUS = 400 + + +class InvalidName(AWSError): + CODE = 'InvalidName' + STATUS = 400 + + +class StateMachineDoesNotExist(AWSError): + CODE = 'StateMachineDoesNotExist' + STATUS = 400 diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py new file mode 100644 index 000000000000..8571fbe9b9c2 --- /dev/null +++ b/moto/stepfunctions/models.py @@ -0,0 +1,121 @@ +import boto +import boto3 +import re +from datetime import datetime +from moto.core import BaseBackend +from moto.core.utils import iso_8601_datetime_without_milliseconds +from .exceptions import AccessDeniedException, InvalidArn, InvalidName, StateMachineDoesNotExist + + +class StateMachine(): + def __init__(self, arn, name, definition, roleArn, tags=None): + self.creation_date = iso_8601_datetime_without_milliseconds(datetime.now()) + self.arn = arn + self.name = name + self.definition = definition + self.roleArn = roleArn + self.tags = tags + + +class StepFunctionBackend(BaseBackend): + + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/stepfunctions.html#SFN.Client.create_state_machine + # A name must not contain: + # whitespace + # brackets < > { } [ ] + # wildcard characters ? * + # special characters " # % \ ^ | ~ ` $ & , ; : / + invalid_chars_for_name = [' ', '{', '}', '[', ']', '<', '>', + '?', '*', + '"', '#', '%', '\\', '^', '|', '~', '`', '$', '&', ',', ';', ':', '/'] + # control characters (U+0000-001F , U+007F-009F ) + invalid_unicodes_for_name = [u'\u0000', u'\u0001', u'\u0002', u'\u0003', u'\u0004', + u'\u0005', u'\u0006', u'\u0007', u'\u0008', u'\u0009', + u'\u000A', u'\u000B', u'\u000C', u'\u000D', u'\u000E', u'\u000F', + u'\u0010', u'\u0011', u'\u0012', u'\u0013', u'\u0014', + u'\u0015', u'\u0016', u'\u0017', u'\u0018', u'\u0019', + u'\u001A', u'\u001B', u'\u001C', u'\u001D', u'\u001E', u'\u001F', + u'\u007F', + u'\u0080', u'\u0081', u'\u0082', u'\u0083', u'\u0084', u'\u0085', + u'\u0086', u'\u0087', u'\u0088', u'\u0089', + u'\u008A', u'\u008B', u'\u008C', u'\u008D', u'\u008E', u'\u008F', + u'\u0090', u'\u0091', u'\u0092', u'\u0093', u'\u0094', u'\u0095', + u'\u0096', u'\u0097', u'\u0098', u'\u0099', + u'\u009A', u'\u009B', u'\u009C', u'\u009D', u'\u009E', u'\u009F'] + accepted_role_arn_format = re.compile('arn:aws:iam:(?P[0-9]{12}):role/.+') + accepted_mchn_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P[0-9]{12}):stateMachine:.+') + + def __init__(self, region_name): + self.state_machines = [] + self.region_name = region_name + self._account_id = None + + def create_state_machine(self, name, definition, roleArn, tags=None): + self._validate_name(name) + self._validate_role_arn(roleArn) + arn = 'arn:aws:states:' + self.region_name + ':' + str(self._get_account_id()) + ':stateMachine:' + name + try: + return self.describe_state_machine(arn) + except StateMachineDoesNotExist: + state_machine = StateMachine(arn, name, definition, roleArn, tags) + self.state_machines.append(state_machine) + return state_machine + + def list_state_machines(self): + return self.state_machines + + def describe_state_machine(self, arn): + self._validate_machine_arn(arn) + sm = next((x for x in self.state_machines if x.arn == arn), None) + if not sm: + raise StateMachineDoesNotExist("State Machine Does Not Exist: '" + arn + "'") + return sm + + def delete_state_machine(self, arn): + self._validate_machine_arn(arn) + sm = next((x for x in self.state_machines if x.arn == arn), None) + if sm: + self.state_machines.remove(sm) + + def reset(self): + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + + def _validate_name(self, name): + if any(invalid_char in name for invalid_char in self.invalid_chars_for_name): + raise InvalidName("Invalid Name: '" + name + "'") + + if any(name.find(char) >= 0 for char in self.invalid_unicodes_for_name): + raise InvalidName("Invalid Name: '" + name + "'") + + def _validate_role_arn(self, role_arn): + self._validate_arn(arn=role_arn, + regex=self.accepted_role_arn_format, + invalid_msg="Invalid Role Arn: '" + role_arn + "'", + access_denied_msg='Cross-account pass role is not allowed.') + + def _validate_machine_arn(self, machine_arn): + self._validate_arn(arn=machine_arn, + regex=self.accepted_mchn_arn_format, + invalid_msg="Invalid Role Arn: '" + machine_arn + "'", + access_denied_msg='User moto is not authorized to access this resource') + + def _validate_arn(self, arn, regex, invalid_msg, access_denied_msg): + match = regex.match(arn) + if not arn or not match: + raise InvalidArn(invalid_msg) + + if self._get_account_id() != match.group('account_id'): + raise AccessDeniedException(access_denied_msg) + + def _get_account_id(self): + if self._account_id: + return self._account_id + sts = boto3.client("sts") + identity = sts.get_caller_identity() + self._account_id = identity['Account'] + return self._account_id + + +stepfunction_backends = {_region.name: StepFunctionBackend(_region.name) for _region in boto.awslambda.regions()} diff --git a/moto/stepfunctions/responses.py b/moto/stepfunctions/responses.py new file mode 100644 index 000000000000..d729a5a38f10 --- /dev/null +++ b/moto/stepfunctions/responses.py @@ -0,0 +1,80 @@ +from __future__ import unicode_literals + +import json + +from moto.core.responses import BaseResponse +from moto.core.utils import amzn_request_id +from .exceptions import AWSError +from .models import stepfunction_backends + + +class StepFunctionResponse(BaseResponse): + + @property + def stepfunction_backend(self): + return stepfunction_backends[self.region] + + @amzn_request_id + def create_state_machine(self): + name = self._get_param('name') + definition = self._get_param('definition') + roleArn = self._get_param('roleArn') + tags = self._get_param('tags') + try: + state_machine = self.stepfunction_backend.create_state_machine(name=name, definition=definition, + roleArn=roleArn, + tags=tags) + response = { + 'creationDate': state_machine.creation_date, + 'stateMachineArn': state_machine.arn + } + return 200, {}, json.dumps(response) + except AWSError as err: + return err.response() + + @amzn_request_id + def list_state_machines(self): + list_all = self.stepfunction_backend.list_state_machines() + list_all = sorted([{'creationDate': sm.creation_date, + 'name': sm.name, + 'stateMachineArn': sm.arn} for sm in list_all], + key=lambda x: x['name']) + response = {'stateMachines': list_all} + return 200, {}, json.dumps(response) + + @amzn_request_id + def describe_state_machine(self): + arn = self._get_param('stateMachineArn') + try: + state_machine = self.stepfunction_backend.describe_state_machine(arn) + response = { + 'creationDate': state_machine.creation_date, + 'stateMachineArn': state_machine.arn, + 'definition': state_machine.definition, + 'name': state_machine.name, + 'roleArn': state_machine.roleArn, + 'status': 'ACTIVE' + } + return 200, {}, json.dumps(response) + except AWSError as err: + return err.response() + + @amzn_request_id + def delete_state_machine(self): + arn = self._get_param('stateMachineArn') + try: + self.stepfunction_backend.delete_state_machine(arn) + return 200, {}, json.dumps('{}') + except AWSError as err: + return err.response() + + @amzn_request_id + def list_tags_for_resource(self): + arn = self._get_param('resourceArn') + try: + state_machine = self.stepfunction_backend.describe_state_machine(arn) + tags = state_machine.tags or [] + except AWSError: + tags = [] + response = {'tags': tags} + return 200, {}, json.dumps(response) diff --git a/moto/stepfunctions/urls.py b/moto/stepfunctions/urls.py new file mode 100644 index 000000000000..f8d5fb1e83f4 --- /dev/null +++ b/moto/stepfunctions/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import StepFunctionResponse + +url_bases = [ + "https?://states.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': StepFunctionResponse.dispatch, +} diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py new file mode 100644 index 000000000000..0b9df50a90a3 --- /dev/null +++ b/tests/test_stepfunctions/test_stepfunctions.py @@ -0,0 +1,276 @@ +from __future__ import unicode_literals + +import boto3 +import sure # noqa +import datetime + +from datetime import datetime +from botocore.exceptions import ClientError +from moto.config.models import DEFAULT_ACCOUNT_ID +from nose.tools import assert_raises + +from moto import mock_sts, mock_stepfunctions + + +region = 'us-east-1' +simple_definition = '{"Comment": "An example of the Amazon States Language using a choice state.",' \ + '"StartAt": "DefaultState",' \ + '"States": ' \ + '{"DefaultState": {"Type": "Fail","Error": "DefaultStateError","Cause": "No Matches!"}}}' +default_stepfunction_role = 'arn:aws:iam:' + str(DEFAULT_ACCOUNT_ID) + ':role/unknown_sf_role' + + +@mock_stepfunctions +@mock_sts +def test_state_machine_creation_succeeds(): + client = boto3.client('stepfunctions', region_name=region) + name = 'example_step_function' + # + response = client.create_state_machine(name=name, + definition=str(simple_definition), + roleArn=default_stepfunction_role) + # + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + response['creationDate'].should.be.a(datetime) + response['stateMachineArn'].should.equal('arn:aws:states:' + region + ':123456789012:stateMachine:' + name) + + +@mock_stepfunctions +def test_state_machine_creation_fails_with_invalid_names(): + client = boto3.client('stepfunctions', region_name=region) + invalid_names = [ + 'with space', + 'withbracket', 'with{bracket', 'with}bracket', 'with[bracket', 'with]bracket', + 'with?wildcard', 'with*wildcard', + 'special"char', 'special#char', 'special%char', 'special\\char', 'special^char', 'special|char', + 'special~char', 'special`char', 'special$char', 'special&char', 'special,char', 'special;char', + 'special:char', 'special/char', + u'uni\u0000code', u'uni\u0001code', u'uni\u0002code', u'uni\u0003code', u'uni\u0004code', + u'uni\u0005code', u'uni\u0006code', u'uni\u0007code', u'uni\u0008code', u'uni\u0009code', + u'uni\u000Acode', u'uni\u000Bcode', u'uni\u000Ccode', + u'uni\u000Dcode', u'uni\u000Ecode', u'uni\u000Fcode', + u'uni\u0010code', u'uni\u0011code', u'uni\u0012code', u'uni\u0013code', u'uni\u0014code', + u'uni\u0015code', u'uni\u0016code', u'uni\u0017code', u'uni\u0018code', u'uni\u0019code', + u'uni\u001Acode', u'uni\u001Bcode', u'uni\u001Ccode', + u'uni\u001Dcode', u'uni\u001Ecode', u'uni\u001Fcode', + u'uni\u007Fcode', + u'uni\u0080code', u'uni\u0081code', u'uni\u0082code', u'uni\u0083code', u'uni\u0084code', + u'uni\u0085code', u'uni\u0086code', u'uni\u0087code', u'uni\u0088code', u'uni\u0089code', + u'uni\u008Acode', u'uni\u008Bcode', u'uni\u008Ccode', + u'uni\u008Dcode', u'uni\u008Ecode', u'uni\u008Fcode', + u'uni\u0090code', u'uni\u0091code', u'uni\u0092code', u'uni\u0093code', u'uni\u0094code', + u'uni\u0095code', u'uni\u0096code', u'uni\u0097code', u'uni\u0098code', u'uni\u0099code', + u'uni\u009Acode', u'uni\u009Bcode', u'uni\u009Ccode', + u'uni\u009Dcode', u'uni\u009Ecode', u'uni\u009Fcode'] + # + + for invalid_name in invalid_names: + with assert_raises(ClientError) as exc: + client.create_state_machine(name=invalid_name, + definition=str(simple_definition), + roleArn=default_stepfunction_role) + exc.exception.response['Error']['Code'].should.equal('InvalidName') + exc.exception.response['Error']['Message'].should.equal("Invalid Name: '" + invalid_name + "'") + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +def test_state_machine_creation_requires_valid_role_arn(): + client = boto3.client('stepfunctions', region_name=region) + name = 'example_step_function' + # + with assert_raises(ClientError) as exc: + client.create_state_machine(name=name, + definition=str(simple_definition), + roleArn='arn:aws:iam:1234:role/unknown_role') + exc.exception.response['Error']['Code'].should.equal('InvalidArn') + exc.exception.response['Error']['Message'].should.equal("Invalid Role Arn: 'arn:aws:iam:1234:role/unknown_role'") + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_creation_requires_role_in_same_account(): + client = boto3.client('stepfunctions', region_name=region) + name = 'example_step_function' + # + with assert_raises(ClientError) as exc: + client.create_state_machine(name=name, + definition=str(simple_definition), + roleArn='arn:aws:iam:000000000000:role/unknown_role') + exc.exception.response['Error']['Code'].should.equal('AccessDeniedException') + exc.exception.response['Error']['Message'].should.equal('Cross-account pass role is not allowed.') + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +def test_state_machine_list_returns_empty_list_by_default(): + client = boto3.client('stepfunctions', region_name=region) + # + list = client.list_state_machines() + list['stateMachines'].should.be.empty + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_returns_created_state_machines(): + client = boto3.client('stepfunctions', region_name=region) + # + machine2 = client.create_state_machine(name='name2', + definition=str(simple_definition), + roleArn=default_stepfunction_role) + machine1 = client.create_state_machine(name='name1', + definition=str(simple_definition), + roleArn=default_stepfunction_role, + tags=[{'key': 'tag_key', 'value': 'tag_value'}]) + list = client.list_state_machines() + # + list['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + list['stateMachines'].should.have.length_of(2) + list['stateMachines'][0]['creationDate'].should.be.a(datetime) + list['stateMachines'][0]['creationDate'].should.equal(machine1['creationDate']) + list['stateMachines'][0]['name'].should.equal('name1') + list['stateMachines'][0]['stateMachineArn'].should.equal(machine1['stateMachineArn']) + list['stateMachines'][1]['creationDate'].should.be.a(datetime) + list['stateMachines'][1]['creationDate'].should.equal(machine2['creationDate']) + list['stateMachines'][1]['name'].should.equal('name2') + list['stateMachines'][1]['stateMachineArn'].should.equal(machine2['stateMachineArn']) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_creation_is_idempotent_by_name(): + client = boto3.client('stepfunctions', region_name=region) + # + client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(1) + # + client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(1) + # + client.create_state_machine(name='diff_name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(2) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_creation_can_be_described_by_name(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + desc = client.describe_state_machine(stateMachineArn=sm['stateMachineArn']) + desc['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + desc['creationDate'].should.equal(sm['creationDate']) + desc['definition'].should.equal(str(simple_definition)) + desc['name'].should.equal('name') + desc['roleArn'].should.equal(default_stepfunction_role) + desc['stateMachineArn'].should.equal(sm['stateMachineArn']) + desc['status'].should.equal('ACTIVE') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_state_machine = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':stateMachine:unknown' + client.describe_state_machine(stateMachineArn=unknown_state_machine) + exc.exception.response['Error']['Code'].should.equal('StateMachineDoesNotExist') + exc.exception.response['Error']['Message'].\ + should.equal("State Machine Does Not Exist: '" + unknown_state_machine + "'") + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_machine_in_different_account(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_state_machine = 'arn:aws:states:' + region + ':000000000000:stateMachine:unknown' + client.describe_state_machine(stateMachineArn=unknown_state_machine) + exc.exception.response['Error']['Code'].should.equal('AccessDeniedException') + exc.exception.response['Error']['Message'].should.contain('is not authorized to access this resource') + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_can_be_deleted(): + client = boto3.client('stepfunctions', region_name=region) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + # + response = client.delete_state_machine(stateMachineArn=sm['stateMachineArn']) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + # + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_can_deleted_nonexisting_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + unknown_state_machine = 'arn:aws:states:' + region + ':123456789012:stateMachine:unknown' + response = client.delete_state_machine(stateMachineArn=unknown_state_machine) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + # + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_deletion_validates_arn(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_account_id = 'arn:aws:states:' + region + ':000000000000:stateMachine:unknown' + client.delete_state_machine(stateMachineArn=unknown_account_id) + exc.exception.response['Error']['Code'].should.equal('AccessDeniedException') + exc.exception.response['Error']['Message'].should.contain('is not authorized to access this resource') + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_tags_for_created_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + machine = client.create_state_machine(name='name1', + definition=str(simple_definition), + roleArn=default_stepfunction_role, + tags=[{'key': 'tag_key', 'value': 'tag_value'}]) + response = client.list_tags_for_resource(resourceArn=machine['stateMachineArn']) + tags = response['tags'] + tags.should.have.length_of(1) + tags[0].should.equal({'key': 'tag_key', 'value': 'tag_value'}) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_tags_for_machine_without_tags(): + client = boto3.client('stepfunctions', region_name=region) + # + machine = client.create_state_machine(name='name1', + definition=str(simple_definition), + roleArn=default_stepfunction_role) + response = client.list_tags_for_resource(resourceArn=machine['stateMachineArn']) + tags = response['tags'] + tags.should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_tags_for_nonexisting_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + non_existing_state_machine = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':stateMachine:unknown' + response = client.list_tags_for_resource(resourceArn=non_existing_state_machine) + tags = response['tags'] + tags.should.have.length_of(0) From 78254cc4f2bf5774d37d79edf9255a35d9e23c49 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Wed, 4 Sep 2019 15:42:42 +0100 Subject: [PATCH 2/4] Step Functions - Execution methods --- IMPLEMENTATION_COVERAGE.md | 10 +- moto/stepfunctions/exceptions.py | 5 + moto/stepfunctions/models.py | 50 ++++++- moto/stepfunctions/responses.py | 60 +++++++- .../test_stepfunctions/test_stepfunctions.py | 138 +++++++++++++++++- 5 files changed, 255 insertions(+), 8 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 7a839fb96a04..fd5ad3f1e6cb 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -6054,20 +6054,20 @@ - [ ] delete_activity - [X] delete_state_machine - [ ] describe_activity -- [ ] describe_execution +- [X] describe_execution - [X] describe_state_machine -- [ ] describe_state_machine_for_execution +- [x] describe_state_machine_for_execution - [ ] get_activity_task - [ ] get_execution_history - [ ] list_activities -- [ ] list_executions +- [X] list_executions - [X] list_state_machines - [X] list_tags_for_resource - [ ] send_task_failure - [ ] send_task_heartbeat - [ ] send_task_success -- [ ] start_execution -- [ ] stop_execution +- [X] start_execution +- [X] stop_execution - [ ] tag_resource - [ ] untag_resource - [ ] update_state_machine diff --git a/moto/stepfunctions/exceptions.py b/moto/stepfunctions/exceptions.py index a7c0897a5070..133d0cc830a8 100644 --- a/moto/stepfunctions/exceptions.py +++ b/moto/stepfunctions/exceptions.py @@ -20,6 +20,11 @@ class AccessDeniedException(AWSError): STATUS = 400 +class ExecutionDoesNotExist(AWSError): + CODE = 'ExecutionDoesNotExist' + STATUS = 400 + + class InvalidArn(AWSError): CODE = 'InvalidArn' STATUS = 400 diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py index 8571fbe9b9c2..fd272624f2cd 100644 --- a/moto/stepfunctions/models.py +++ b/moto/stepfunctions/models.py @@ -4,7 +4,8 @@ from datetime import datetime from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime_without_milliseconds -from .exceptions import AccessDeniedException, InvalidArn, InvalidName, StateMachineDoesNotExist +from uuid import uuid4 +from .exceptions import AccessDeniedException, ExecutionDoesNotExist, InvalidArn, InvalidName, StateMachineDoesNotExist class StateMachine(): @@ -17,6 +18,22 @@ def __init__(self, arn, name, definition, roleArn, tags=None): self.tags = tags +class Execution(): + def __init__(self, region_name, account_id, state_machine_name, execution_name, state_machine_arn): + execution_arn = 'arn:aws:states:{}:{}:execution:{}:{}' + execution_arn = execution_arn.format(region_name, account_id, state_machine_name, execution_name) + self.execution_arn = execution_arn + self.name = execution_name + self.start_date = iso_8601_datetime_without_milliseconds(datetime.now()) + self.state_machine_arn = state_machine_arn + self.status = 'RUNNING' + self.stop_date = None + + def stop(self): + self.status = 'SUCCEEDED' + self.stop_date = iso_8601_datetime_without_milliseconds(datetime.now()) + + class StepFunctionBackend(BaseBackend): # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/stepfunctions.html#SFN.Client.create_state_machine @@ -44,9 +61,11 @@ class StepFunctionBackend(BaseBackend): u'\u009A', u'\u009B', u'\u009C', u'\u009D', u'\u009E', u'\u009F'] accepted_role_arn_format = re.compile('arn:aws:iam:(?P[0-9]{12}):role/.+') accepted_mchn_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P[0-9]{12}):stateMachine:.+') + accepted_exec_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P[0-9]{12}):execution:.+') def __init__(self, region_name): self.state_machines = [] + self.executions = [] self.region_name = region_name self._account_id = None @@ -77,6 +96,29 @@ def delete_state_machine(self, arn): if sm: self.state_machines.remove(sm) + def start_execution(self, state_machine_arn): + state_machine_name = self.describe_state_machine(state_machine_arn).name + execution = Execution(region_name=self.region_name, account_id=self._get_account_id(), state_machine_name=state_machine_name, execution_name=str(uuid4()), state_machine_arn=state_machine_arn) + self.executions.append(execution) + return execution + + def stop_execution(self, execution_arn): + execution = next((x for x in self.executions if x.execution_arn == execution_arn), None) + if not execution: + raise ExecutionDoesNotExist("Execution Does Not Exist: '" + execution_arn + "'") + execution.stop() + return execution + + def list_executions(self, state_machine_arn): + return [execution for execution in self.executions if execution.state_machine_arn == state_machine_arn] + + def describe_execution(self, arn): + self._validate_execution_arn(arn) + exctn = next((x for x in self.executions if x.execution_arn == arn), None) + if not exctn: + raise ExecutionDoesNotExist("Execution Does Not Exist: '" + arn + "'") + return exctn + def reset(self): region_name = self.region_name self.__dict__ = {} @@ -101,6 +143,12 @@ def _validate_machine_arn(self, machine_arn): invalid_msg="Invalid Role Arn: '" + machine_arn + "'", access_denied_msg='User moto is not authorized to access this resource') + def _validate_execution_arn(self, execution_arn): + self._validate_arn(arn=execution_arn, + regex=self.accepted_exec_arn_format, + invalid_msg="Execution Does Not Exist: '" + execution_arn + "'", + access_denied_msg='User moto is not authorized to access this resource') + def _validate_arn(self, arn, regex, invalid_msg, access_denied_msg): match = regex.match(arn) if not arn or not match: diff --git a/moto/stepfunctions/responses.py b/moto/stepfunctions/responses.py index d729a5a38f10..0a170aa57181 100644 --- a/moto/stepfunctions/responses.py +++ b/moto/stepfunctions/responses.py @@ -45,8 +45,12 @@ def list_state_machines(self): @amzn_request_id def describe_state_machine(self): arn = self._get_param('stateMachineArn') + return self._describe_state_machine(arn) + + @amzn_request_id + def _describe_state_machine(self, state_machine_arn): try: - state_machine = self.stepfunction_backend.describe_state_machine(arn) + state_machine = self.stepfunction_backend.describe_state_machine(state_machine_arn) response = { 'creationDate': state_machine.creation_date, 'stateMachineArn': state_machine.arn, @@ -78,3 +82,57 @@ def list_tags_for_resource(self): tags = [] response = {'tags': tags} return 200, {}, json.dumps(response) + + @amzn_request_id + def start_execution(self): + arn = self._get_param('stateMachineArn') + execution = self.stepfunction_backend.start_execution(arn) + response = {'executionArn': execution.execution_arn, + 'startDate': execution.start_date} + return 200, {}, json.dumps(response) + + @amzn_request_id + def list_executions(self): + arn = self._get_param('stateMachineArn') + state_machine = self.stepfunction_backend.describe_state_machine(arn) + executions = self.stepfunction_backend.list_executions(arn) + executions = [{'executionArn': execution.execution_arn, + 'name': execution.name, + 'startDate': execution.start_date, + 'stateMachineArn': state_machine.arn, + 'status': execution.status} for execution in executions] + return 200, {}, json.dumps({'executions': executions}) + + @amzn_request_id + def describe_execution(self): + arn = self._get_param('executionArn') + try: + execution = self.stepfunction_backend.describe_execution(arn) + response = { + 'executionArn': arn, + 'input': '{}', + 'name': execution.name, + 'startDate': execution.start_date, + 'stateMachineArn': execution.state_machine_arn, + 'status': execution.status, + 'stopDate': execution.stop_date + } + return 200, {}, json.dumps(response) + except AWSError as err: + return err.response() + + @amzn_request_id + def describe_state_machine_for_execution(self): + arn = self._get_param('executionArn') + try: + execution = self.stepfunction_backend.describe_execution(arn) + return self._describe_state_machine(execution.state_machine_arn) + except AWSError as err: + return err.response() + + @amzn_request_id + def stop_execution(self): + arn = self._get_param('executionArn') + execution = self.stepfunction_backend.stop_execution(arn) + response = {'stopDate': execution.stop_date} + return 200, {}, json.dumps(response) diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py index 0b9df50a90a3..bf5c92570c78 100644 --- a/tests/test_stepfunctions/test_stepfunctions.py +++ b/tests/test_stepfunctions/test_stepfunctions.py @@ -157,7 +157,7 @@ def test_state_machine_creation_is_idempotent_by_name(): @mock_stepfunctions @mock_sts -def test_state_machine_creation_can_be_described_by_name(): +def test_state_machine_creation_can_be_described(): client = boto3.client('stepfunctions', region_name=region) # sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) @@ -274,3 +274,139 @@ def test_state_machine_list_tags_for_nonexisting_machine(): response = client.list_tags_for_resource(resourceArn=non_existing_state_machine) tags = response['tags'] tags.should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_start_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + # + execution['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + expected_exec_name = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:name:[a-zA-Z0-9-]+' + execution['executionArn'].should.match(expected_exec_name) + execution['startDate'].should.be.a(datetime) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_executions(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + execution_arn = execution['executionArn'] + execution_name = execution_arn[execution_arn.rindex(':')+1:] + executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) + # + executions['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + executions['executions'].should.have.length_of(1) + executions['executions'][0]['executionArn'].should.equal(execution_arn) + executions['executions'][0]['name'].should.equal(execution_name) + executions['executions'][0]['startDate'].should.equal(execution['startDate']) + executions['executions'][0]['stateMachineArn'].should.equal(sm['stateMachineArn']) + executions['executions'][0]['status'].should.equal('RUNNING') + executions['executions'][0].shouldnt.have('stopDate') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_executions_when_none_exist(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) + # + executions['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + executions['executions'].should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_describe_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + description = client.describe_execution(executionArn=execution['executionArn']) + # + description['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + description['executionArn'].should.equal(execution['executionArn']) + description['input'].should.equal("{}") + description['name'].shouldnt.be.empty + description['startDate'].should.equal(execution['startDate']) + description['stateMachineArn'].should.equal(sm['stateMachineArn']) + description['status'].should.equal('RUNNING') + description.shouldnt.have('stopDate') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_execution = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:unknown' + client.describe_execution(executionArn=unknown_execution) + exc.exception.response['Error']['Code'].should.equal('ExecutionDoesNotExist') + exc.exception.response['Error']['Message'].should.equal("Execution Does Not Exist: '" + unknown_execution + "'") + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_can_be_described_by_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + desc = client.describe_state_machine_for_execution(executionArn=execution['executionArn']) + desc['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + desc['definition'].should.equal(str(simple_definition)) + desc['name'].should.equal('name') + desc['roleArn'].should.equal(default_stepfunction_role) + desc['stateMachineArn'].should.equal(sm['stateMachineArn']) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_execution = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:unknown' + client.describe_state_machine_for_execution(executionArn=unknown_execution) + exc.exception.response['Error']['Code'].should.equal('ExecutionDoesNotExist') + exc.exception.response['Error']['Message'].should.equal("Execution Does Not Exist: '" + unknown_execution + "'") + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_stop_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + start = client.start_execution(stateMachineArn=sm['stateMachineArn']) + stop = client.stop_execution(executionArn=start['executionArn']) + print(stop) + # + stop['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + stop['stopDate'].should.be.a(datetime) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_describe_execution_after_stoppage(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + client.stop_execution(executionArn=execution['executionArn']) + description = client.describe_execution(executionArn=execution['executionArn']) + # + description['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + description['status'].should.equal('SUCCEEDED') + description['stopDate'].should.be.a(datetime) From 6a1a8df7ccd172f67308b99f6ccf7b1d2d4d1f6d Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Sat, 7 Sep 2019 16:37:55 +0100 Subject: [PATCH 3/4] Step Functions - Simplify tests --- moto/stepfunctions/exceptions.py | 21 ++-- moto/stepfunctions/models.py | 22 ++-- .../test_stepfunctions/test_stepfunctions.py | 116 +++++++----------- 3 files changed, 59 insertions(+), 100 deletions(-) diff --git a/moto/stepfunctions/exceptions.py b/moto/stepfunctions/exceptions.py index 133d0cc830a8..8af4686c7433 100644 --- a/moto/stepfunctions/exceptions.py +++ b/moto/stepfunctions/exceptions.py @@ -3,38 +3,33 @@ class AWSError(Exception): - CODE = None + TYPE = None STATUS = 400 - def __init__(self, message, code=None, status=None): + def __init__(self, message, type=None, status=None): self.message = message - self.code = code if code is not None else self.CODE + self.type = type if type is not None else self.TYPE self.status = status if status is not None else self.STATUS def response(self): - return json.dumps({'__type': self.code, 'message': self.message}), dict(status=self.status) - - -class AccessDeniedException(AWSError): - CODE = 'AccessDeniedException' - STATUS = 400 + return json.dumps({'__type': self.type, 'message': self.message}), dict(status=self.status) class ExecutionDoesNotExist(AWSError): - CODE = 'ExecutionDoesNotExist' + TYPE = 'ExecutionDoesNotExist' STATUS = 400 class InvalidArn(AWSError): - CODE = 'InvalidArn' + TYPE = 'InvalidArn' STATUS = 400 class InvalidName(AWSError): - CODE = 'InvalidName' + TYPE = 'InvalidName' STATUS = 400 class StateMachineDoesNotExist(AWSError): - CODE = 'StateMachineDoesNotExist' + TYPE = 'StateMachineDoesNotExist' STATUS = 400 diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py index fd272624f2cd..8db9db1a17c2 100644 --- a/moto/stepfunctions/models.py +++ b/moto/stepfunctions/models.py @@ -5,7 +5,7 @@ from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime_without_milliseconds from uuid import uuid4 -from .exceptions import AccessDeniedException, ExecutionDoesNotExist, InvalidArn, InvalidName, StateMachineDoesNotExist +from .exceptions import ExecutionDoesNotExist, InvalidArn, InvalidName, StateMachineDoesNotExist class StateMachine(): @@ -98,7 +98,11 @@ def delete_state_machine(self, arn): def start_execution(self, state_machine_arn): state_machine_name = self.describe_state_machine(state_machine_arn).name - execution = Execution(region_name=self.region_name, account_id=self._get_account_id(), state_machine_name=state_machine_name, execution_name=str(uuid4()), state_machine_arn=state_machine_arn) + execution = Execution(region_name=self.region_name, + account_id=self._get_account_id(), + state_machine_name=state_machine_name, + execution_name=str(uuid4()), + state_machine_arn=state_machine_arn) self.executions.append(execution) return execution @@ -134,29 +138,23 @@ def _validate_name(self, name): def _validate_role_arn(self, role_arn): self._validate_arn(arn=role_arn, regex=self.accepted_role_arn_format, - invalid_msg="Invalid Role Arn: '" + role_arn + "'", - access_denied_msg='Cross-account pass role is not allowed.') + invalid_msg="Invalid Role Arn: '" + role_arn + "'") def _validate_machine_arn(self, machine_arn): self._validate_arn(arn=machine_arn, regex=self.accepted_mchn_arn_format, - invalid_msg="Invalid Role Arn: '" + machine_arn + "'", - access_denied_msg='User moto is not authorized to access this resource') + invalid_msg="Invalid Role Arn: '" + machine_arn + "'") def _validate_execution_arn(self, execution_arn): self._validate_arn(arn=execution_arn, regex=self.accepted_exec_arn_format, - invalid_msg="Execution Does Not Exist: '" + execution_arn + "'", - access_denied_msg='User moto is not authorized to access this resource') + invalid_msg="Execution Does Not Exist: '" + execution_arn + "'") - def _validate_arn(self, arn, regex, invalid_msg, access_denied_msg): + def _validate_arn(self, arn, regex, invalid_msg): match = regex.match(arn) if not arn or not match: raise InvalidArn(invalid_msg) - if self._get_account_id() != match.group('account_id'): - raise AccessDeniedException(access_denied_msg) - def _get_account_id(self): if self._account_id: return self._account_id diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py index bf5c92570c78..10953ce2dce9 100644 --- a/tests/test_stepfunctions/test_stepfunctions.py +++ b/tests/test_stepfunctions/test_stepfunctions.py @@ -6,7 +6,6 @@ from datetime import datetime from botocore.exceptions import ClientError -from moto.config.models import DEFAULT_ACCOUNT_ID from nose.tools import assert_raises from moto import mock_sts, mock_stepfunctions @@ -17,7 +16,7 @@ '"StartAt": "DefaultState",' \ '"States": ' \ '{"DefaultState": {"Type": "Fail","Error": "DefaultStateError","Cause": "No Matches!"}}}' -default_stepfunction_role = 'arn:aws:iam:' + str(DEFAULT_ACCOUNT_ID) + ':role/unknown_sf_role' +account_id = None @mock_stepfunctions @@ -28,7 +27,7 @@ def test_state_machine_creation_succeeds(): # response = client.create_state_machine(name=name, definition=str(simple_definition), - roleArn=default_stepfunction_role) + roleArn=_get_default_role()) # response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) response['creationDate'].should.be.a(datetime) @@ -68,10 +67,7 @@ def test_state_machine_creation_fails_with_invalid_names(): with assert_raises(ClientError) as exc: client.create_state_machine(name=invalid_name, definition=str(simple_definition), - roleArn=default_stepfunction_role) - exc.exception.response['Error']['Code'].should.equal('InvalidName') - exc.exception.response['Error']['Message'].should.equal("Invalid Name: '" + invalid_name + "'") - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + roleArn=_get_default_role()) @mock_stepfunctions @@ -83,24 +79,6 @@ def test_state_machine_creation_requires_valid_role_arn(): client.create_state_machine(name=name, definition=str(simple_definition), roleArn='arn:aws:iam:1234:role/unknown_role') - exc.exception.response['Error']['Code'].should.equal('InvalidArn') - exc.exception.response['Error']['Message'].should.equal("Invalid Role Arn: 'arn:aws:iam:1234:role/unknown_role'") - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) - - -@mock_stepfunctions -@mock_sts -def test_state_machine_creation_requires_role_in_same_account(): - client = boto3.client('stepfunctions', region_name=region) - name = 'example_step_function' - # - with assert_raises(ClientError) as exc: - client.create_state_machine(name=name, - definition=str(simple_definition), - roleArn='arn:aws:iam:000000000000:role/unknown_role') - exc.exception.response['Error']['Code'].should.equal('AccessDeniedException') - exc.exception.response['Error']['Message'].should.equal('Cross-account pass role is not allowed.') - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) @mock_stepfunctions @@ -118,10 +96,10 @@ def test_state_machine_list_returns_created_state_machines(): # machine2 = client.create_state_machine(name='name2', definition=str(simple_definition), - roleArn=default_stepfunction_role) + roleArn=_get_default_role()) machine1 = client.create_state_machine(name='name1', definition=str(simple_definition), - roleArn=default_stepfunction_role, + roleArn=_get_default_role(), tags=[{'key': 'tag_key', 'value': 'tag_value'}]) list = client.list_state_machines() # @@ -142,15 +120,15 @@ def test_state_machine_list_returns_created_state_machines(): def test_state_machine_creation_is_idempotent_by_name(): client = boto3.client('stepfunctions', region_name=region) # - client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) sm_list = client.list_state_machines() sm_list['stateMachines'].should.have.length_of(1) # - client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) sm_list = client.list_state_machines() sm_list['stateMachines'].should.have.length_of(1) # - client.create_state_machine(name='diff_name', definition=str(simple_definition), roleArn=default_stepfunction_role) + client.create_state_machine(name='diff_name', definition=str(simple_definition), roleArn=_get_default_role()) sm_list = client.list_state_machines() sm_list['stateMachines'].should.have.length_of(2) @@ -160,13 +138,13 @@ def test_state_machine_creation_is_idempotent_by_name(): def test_state_machine_creation_can_be_described(): client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) desc = client.describe_state_machine(stateMachineArn=sm['stateMachineArn']) desc['ResponseMetadata']['HTTPStatusCode'].should.equal(200) desc['creationDate'].should.equal(sm['creationDate']) desc['definition'].should.equal(str(simple_definition)) desc['name'].should.equal('name') - desc['roleArn'].should.equal(default_stepfunction_role) + desc['roleArn'].should.equal(_get_default_role()) desc['stateMachineArn'].should.equal(sm['stateMachineArn']) desc['status'].should.equal('ACTIVE') @@ -177,12 +155,8 @@ def test_state_machine_throws_error_when_describing_unknown_machine(): client = boto3.client('stepfunctions', region_name=region) # with assert_raises(ClientError) as exc: - unknown_state_machine = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':stateMachine:unknown' + unknown_state_machine = 'arn:aws:states:' + region + ':' + _get_account_id() + ':stateMachine:unknown' client.describe_state_machine(stateMachineArn=unknown_state_machine) - exc.exception.response['Error']['Code'].should.equal('StateMachineDoesNotExist') - exc.exception.response['Error']['Message'].\ - should.equal("State Machine Does Not Exist: '" + unknown_state_machine + "'") - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) @mock_stepfunctions @@ -193,16 +167,13 @@ def test_state_machine_throws_error_when_describing_machine_in_different_account with assert_raises(ClientError) as exc: unknown_state_machine = 'arn:aws:states:' + region + ':000000000000:stateMachine:unknown' client.describe_state_machine(stateMachineArn=unknown_state_machine) - exc.exception.response['Error']['Code'].should.equal('AccessDeniedException') - exc.exception.response['Error']['Message'].should.contain('is not authorized to access this resource') - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) @mock_stepfunctions @mock_sts def test_state_machine_can_be_deleted(): client = boto3.client('stepfunctions', region_name=region) - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) # response = client.delete_state_machine(stateMachineArn=sm['stateMachineArn']) response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) @@ -224,19 +195,6 @@ def test_state_machine_can_deleted_nonexisting_machine(): sm_list['stateMachines'].should.have.length_of(0) -@mock_stepfunctions -@mock_sts -def test_state_machine_deletion_validates_arn(): - client = boto3.client('stepfunctions', region_name=region) - # - with assert_raises(ClientError) as exc: - unknown_account_id = 'arn:aws:states:' + region + ':000000000000:stateMachine:unknown' - client.delete_state_machine(stateMachineArn=unknown_account_id) - exc.exception.response['Error']['Code'].should.equal('AccessDeniedException') - exc.exception.response['Error']['Message'].should.contain('is not authorized to access this resource') - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) - - @mock_stepfunctions @mock_sts def test_state_machine_list_tags_for_created_machine(): @@ -244,7 +202,7 @@ def test_state_machine_list_tags_for_created_machine(): # machine = client.create_state_machine(name='name1', definition=str(simple_definition), - roleArn=default_stepfunction_role, + roleArn=_get_default_role(), tags=[{'key': 'tag_key', 'value': 'tag_value'}]) response = client.list_tags_for_resource(resourceArn=machine['stateMachineArn']) tags = response['tags'] @@ -259,7 +217,7 @@ def test_state_machine_list_tags_for_machine_without_tags(): # machine = client.create_state_machine(name='name1', definition=str(simple_definition), - roleArn=default_stepfunction_role) + roleArn=_get_default_role()) response = client.list_tags_for_resource(resourceArn=machine['stateMachineArn']) tags = response['tags'] tags.should.have.length_of(0) @@ -270,7 +228,7 @@ def test_state_machine_list_tags_for_machine_without_tags(): def test_state_machine_list_tags_for_nonexisting_machine(): client = boto3.client('stepfunctions', region_name=region) # - non_existing_state_machine = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':stateMachine:unknown' + non_existing_state_machine = 'arn:aws:states:' + region + ':' + _get_account_id() + ':stateMachine:unknown' response = client.list_tags_for_resource(resourceArn=non_existing_state_machine) tags = response['tags'] tags.should.have.length_of(0) @@ -281,11 +239,11 @@ def test_state_machine_list_tags_for_nonexisting_machine(): def test_state_machine_start_execution(): client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) # execution['ResponseMetadata']['HTTPStatusCode'].should.equal(200) - expected_exec_name = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:name:[a-zA-Z0-9-]+' + expected_exec_name = 'arn:aws:states:' + region + ':' + _get_account_id() + ':execution:name:[a-zA-Z0-9-]+' execution['executionArn'].should.match(expected_exec_name) execution['startDate'].should.be.a(datetime) @@ -295,7 +253,7 @@ def test_state_machine_start_execution(): def test_state_machine_list_executions(): client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) execution_arn = execution['executionArn'] execution_name = execution_arn[execution_arn.rindex(':')+1:] @@ -316,7 +274,7 @@ def test_state_machine_list_executions(): def test_state_machine_list_executions_when_none_exist(): client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) # executions['ResponseMetadata']['HTTPStatusCode'].should.equal(200) @@ -328,7 +286,7 @@ def test_state_machine_list_executions_when_none_exist(): def test_state_machine_describe_execution(): client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) description = client.describe_execution(executionArn=execution['executionArn']) # @@ -348,11 +306,8 @@ def test_state_machine_throws_error_when_describing_unknown_machine(): client = boto3.client('stepfunctions', region_name=region) # with assert_raises(ClientError) as exc: - unknown_execution = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:unknown' + unknown_execution = 'arn:aws:states:' + region + ':' + _get_account_id() + ':execution:unknown' client.describe_execution(executionArn=unknown_execution) - exc.exception.response['Error']['Code'].should.equal('ExecutionDoesNotExist') - exc.exception.response['Error']['Message'].should.equal("Execution Does Not Exist: '" + unknown_execution + "'") - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) @mock_stepfunctions @@ -360,13 +315,13 @@ def test_state_machine_throws_error_when_describing_unknown_machine(): def test_state_machine_can_be_described_by_execution(): client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) desc = client.describe_state_machine_for_execution(executionArn=execution['executionArn']) desc['ResponseMetadata']['HTTPStatusCode'].should.equal(200) desc['definition'].should.equal(str(simple_definition)) desc['name'].should.equal('name') - desc['roleArn'].should.equal(default_stepfunction_role) + desc['roleArn'].should.equal(_get_default_role()) desc['stateMachineArn'].should.equal(sm['stateMachineArn']) @@ -376,11 +331,8 @@ def test_state_machine_throws_error_when_describing_unknown_execution(): client = boto3.client('stepfunctions', region_name=region) # with assert_raises(ClientError) as exc: - unknown_execution = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:unknown' + unknown_execution = 'arn:aws:states:' + region + ':' + _get_account_id() + ':execution:unknown' client.describe_state_machine_for_execution(executionArn=unknown_execution) - exc.exception.response['Error']['Code'].should.equal('ExecutionDoesNotExist') - exc.exception.response['Error']['Message'].should.equal("Execution Does Not Exist: '" + unknown_execution + "'") - exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) @mock_stepfunctions @@ -388,10 +340,9 @@ def test_state_machine_throws_error_when_describing_unknown_execution(): def test_state_machine_stop_execution(): client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) start = client.start_execution(stateMachineArn=sm['stateMachineArn']) stop = client.stop_execution(executionArn=start['executionArn']) - print(stop) # stop['ResponseMetadata']['HTTPStatusCode'].should.equal(200) stop['stopDate'].should.be.a(datetime) @@ -400,9 +351,10 @@ def test_state_machine_stop_execution(): @mock_stepfunctions @mock_sts def test_state_machine_describe_execution_after_stoppage(): + account_id client = boto3.client('stepfunctions', region_name=region) # - sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) client.stop_execution(executionArn=execution['executionArn']) description = client.describe_execution(executionArn=execution['executionArn']) @@ -410,3 +362,17 @@ def test_state_machine_describe_execution_after_stoppage(): description['ResponseMetadata']['HTTPStatusCode'].should.equal(200) description['status'].should.equal('SUCCEEDED') description['stopDate'].should.be.a(datetime) + + +def _get_account_id(): + global account_id + if account_id: + return account_id + sts = boto3.client("sts") + identity = sts.get_caller_identity() + account_id = identity['Account'] + return account_id + + +def _get_default_role(): + return 'arn:aws:iam:' + _get_account_id() + ':role/unknown_sf_role' From 38455c8e1943e2ffb8bf1f2306095b3c08cf559a Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Tue, 24 Sep 2019 14:36:34 +0100 Subject: [PATCH 4/4] Step Functions - Remove STS-client and refer to hardcoded account-id --- moto/stepfunctions/models.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py index 8db9db1a17c2..7784919b07ed 100644 --- a/moto/stepfunctions/models.py +++ b/moto/stepfunctions/models.py @@ -1,9 +1,9 @@ import boto -import boto3 import re from datetime import datetime from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime_without_milliseconds +from moto.sts.models import ACCOUNT_ID from uuid import uuid4 from .exceptions import ExecutionDoesNotExist, InvalidArn, InvalidName, StateMachineDoesNotExist @@ -156,12 +156,7 @@ def _validate_arn(self, arn, regex, invalid_msg): raise InvalidArn(invalid_msg) def _get_account_id(self): - if self._account_id: - return self._account_id - sts = boto3.client("sts") - identity = sts.get_caller_identity() - self._account_id = identity['Account'] - return self._account_id + return ACCOUNT_ID stepfunction_backends = {_region.name: StepFunctionBackend(_region.name) for _region in boto.awslambda.regions()}