From edbc57e00d74bdfc39a604e692c5607ca57ac705 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sat, 14 Jul 2018 11:35:37 -0700 Subject: [PATCH 01/16] add support for AWS Organizations endpoints covers so far: - create_organization - describe_organization - create_account - describe_account - list_accounts all tests passing. could use some advise from maintaners. --- moto/__init__.py | 1 + moto/backends.py | 2 + moto/organizations/__init__.py | 6 + moto/organizations/models.py | 131 ++++++++++++ moto/organizations/responses.py | 48 +++++ moto/organizations/urls.py | 10 + moto/organizations/utils.py | 34 +++ tests/test_organizations/__init__.py | 0 tests/test_organizations/object_syntax.py | 24 +++ .../test_organizations_boto3.py | 193 ++++++++++++++++++ .../test_organizations_utils.py | 26 +++ 11 files changed, 475 insertions(+) create mode 100644 moto/organizations/__init__.py create mode 100644 moto/organizations/models.py create mode 100644 moto/organizations/responses.py create mode 100644 moto/organizations/urls.py create mode 100644 moto/organizations/utils.py create mode 100644 tests/test_organizations/__init__.py create mode 100644 tests/test_organizations/object_syntax.py create mode 100644 tests/test_organizations/test_organizations_boto3.py create mode 100644 tests/test_organizations/test_organizations_utils.py diff --git a/moto/__init__.py b/moto/__init__.py index 0ce5e54d1..2301b7ca1 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -27,6 +27,7 @@ from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa from .iam import mock_iam, mock_iam_deprecated # flake8: noqa from .kinesis import mock_kinesis, mock_kinesis_deprecated # flake8: noqa from .kms import mock_kms, mock_kms_deprecated # flake8: noqa +from .organizations import mock_organizations # flake8: noqa from .opsworks import mock_opsworks, mock_opsworks_deprecated # flake8: noqa from .polly import mock_polly # flake8: noqa from .rds import mock_rds, mock_rds_deprecated # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index cd8fe174f..25fcec09a 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -26,6 +26,7 @@ from moto.kinesis import kinesis_backends from moto.kms import kms_backends from moto.logs import logs_backends from moto.opsworks import opsworks_backends +from moto.organizations import organizations_backends from moto.polly import polly_backends from moto.rds2 import rds2_backends from moto.redshift import redshift_backends @@ -72,6 +73,7 @@ BACKENDS = { 'kinesis': kinesis_backends, 'kms': kms_backends, 'opsworks': opsworks_backends, + 'organizations': organizations_backends, 'polly': polly_backends, 'redshift': redshift_backends, 'rds': rds2_backends, diff --git a/moto/organizations/__init__.py b/moto/organizations/__init__.py new file mode 100644 index 000000000..372782dd3 --- /dev/null +++ b/moto/organizations/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import organizations_backend +from ..core.models import base_decorator + +organizations_backends = {"global": organizations_backend} +mock_organizations = base_decorator(organizations_backends) diff --git a/moto/organizations/models.py b/moto/organizations/models.py new file mode 100644 index 000000000..50212af29 --- /dev/null +++ b/moto/organizations/models.py @@ -0,0 +1,131 @@ +from __future__ import unicode_literals + +import datetime +import time + +from moto.core import BaseBackend, BaseModel +from moto.core.utils import unix_time +from moto.organizations import utils + +MASTER_ACCOUNT_ID = '123456789012' +MASTER_ACCOUNT_EMAIL = 'fakeorg@moto-example.com' +ORGANIZATION_ARN_FORMAT = 'arn:aws:organizations::{0}:organization/{1}' +MASTER_ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{0}' +ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{2}' + + +class FakeOrganization(BaseModel): + + def __init__(self, feature_set): + self.id = utils.make_random_org_id() + self.feature_set = feature_set + self.master_account_id = MASTER_ACCOUNT_ID + self.master_account_email = MASTER_ACCOUNT_EMAIL + self.available_policy_types = [{ + 'Type': 'SERVICE_CONTROL_POLICY', + 'Status': 'ENABLED' + }] + + @property + def arn(self): + return ORGANIZATION_ARN_FORMAT.format(self.master_account_id, self.id) + + @property + def master_account_arn(self): + return MASTER_ACCOUNT_ARN_FORMAT.format(self.master_account_id, self.id) + + def _describe(self): + return { + 'Organization': { + 'Id': self.id, + 'Arn': self.arn, + 'FeatureSet': self.feature_set, + 'MasterAccountArn': self.master_account_arn, + 'MasterAccountId': self.master_account_id, + 'MasterAccountEmail': self.master_account_email, + 'AvailablePolicyTypes': self.available_policy_types, + } + } + + +class FakeAccount(BaseModel): + + def __init__(self, organization, **kwargs): + self.organization_id = organization.id + self.master_account_id = organization.master_account_id + self.create_account_status_id = utils.make_random_create_account_status_id() + self.account_id = utils.make_random_account_id() + self.account_name = kwargs['AccountName'] + self.email = kwargs['Email'] + self.create_time = datetime.datetime.utcnow() + self.status = 'ACTIVE' + self.joined_method = 'CREATED' + + @property + def arn(self): + return ACCOUNT_ARN_FORMAT.format( + self.master_account_id, + self.organization_id, + self.account_id + ) + + @property + def create_account_status(self): + return { + 'CreateAccountStatus': { + 'Id': self.create_account_status_id, + 'AccountName': self.account_name, + 'State': 'SUCCEEDED', + 'RequestedTimestamp': unix_time(self.create_time), + 'CompletedTimestamp': unix_time(self.create_time), + 'AccountId': self.account_id, + } + } + + def describe(self): + return { + 'Account': { + 'Id': self.account_id, + 'Arn': self.arn, + 'Email': self.email, + 'Name': self.account_name, + 'Status': self.status, + 'JoinedMethod': self.joined_method, + 'JoinedTimestamp': unix_time(self.create_time), + } + } + + +class OrganizationsBackend(BaseBackend): + + def __init__(self): + self.org = None + self.accounts = [] + + def create_organization(self, **kwargs): + self.org = FakeOrganization(kwargs['FeatureSet']) + return self.org._describe() + + def describe_organization(self): + return self.org._describe() + + def create_account(self, **kwargs): + new_account = FakeAccount(self.org, **kwargs) + self.accounts.append(new_account) + return new_account.create_account_status + + def describe_account(self, **kwargs): + account = [account for account in self.accounts + if account.account_id == kwargs['AccountId']][0] + return account.describe() + + def list_accounts(self): + return dict( + Accounts=[account.describe()['Account'] for account in self.accounts] + ) + + +organizations_backend = OrganizationsBackend() + + + diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py new file mode 100644 index 000000000..10288dedb --- /dev/null +++ b/moto/organizations/responses.py @@ -0,0 +1,48 @@ +from __future__ import unicode_literals +import json + +from moto.core.responses import BaseResponse +from .models import organizations_backend + + +class OrganizationsResponse(BaseResponse): + + @property + def organizations_backend(self): + return organizations_backend + + @property + def request_params(self): + try: + return json.loads(self.body) + except ValueError: + return {} + + def _get_param(self, param, default=None): + return self.request_params.get(param, default) + + def create_organization(self): + return json.dumps( + self.organizations_backend.create_organization(**self.request_params) + ) + + def describe_organization(self): + return json.dumps( + self.organizations_backend.describe_organization() + ) + + def create_account(self): + return json.dumps( + self.organizations_backend.create_account(**self.request_params) + ) + + def describe_account(self): + return json.dumps( + self.organizations_backend.describe_account(**self.request_params) + ) + + def list_accounts(self): + return json.dumps( + self.organizations_backend.list_accounts() + ) + diff --git a/moto/organizations/urls.py b/moto/organizations/urls.py new file mode 100644 index 000000000..7911f5b53 --- /dev/null +++ b/moto/organizations/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import OrganizationsResponse + +url_bases = [ + "https?://organizations.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': OrganizationsResponse.dispatch, +} diff --git a/moto/organizations/utils.py b/moto/organizations/utils.py new file mode 100644 index 000000000..5916dc5ea --- /dev/null +++ b/moto/organizations/utils.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals + +import random +import string + +CHARSET=string.ascii_lowercase + string.digits +ORG_ID_SIZE = 10 +ROOT_ID_SIZE = 4 +ACCOUNT_ID_SIZE = 12 +CREATE_ACCOUNT_STATUS_ID_SIZE = 8 + + +def make_random_org_id(): + # The regex pattern for an organization ID string requires "o-" + # followed by from 10 to 32 lower-case letters or digits. + # e.g. 'o-vipjnq5z86' + return 'o-' + ''.join(random.choice(CHARSET) for x in range(ORG_ID_SIZE)) + +def make_random_root_id(): + # The regex pattern for a root ID string requires "r-" followed by + # from 4 to 32 lower-case letters or digits. + # e.g. 'r-3zwx' + return 'r-' + ''.join(random.choice(CHARSET) for x in range(ROOT_ID_SIZE)) + +def make_random_account_id(): + # The regex pattern for an account ID string requires exactly 12 digits. + # e.g. '488633172133' + return ''.join([random.choice(string.digits) for n in range(ACCOUNT_ID_SIZE)]) + +def make_random_create_account_status_id(): + # The regex pattern for an create account request ID string requires + # "car-" followed by from 8 to 32 lower-case letters or digits. + # e.g. 'car-35gxzwrp' + return 'car-' + ''.join(random.choice(CHARSET) for x in range(CREATE_ACCOUNT_STATUS_ID_SIZE)) diff --git a/tests/test_organizations/__init__.py b/tests/test_organizations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_organizations/object_syntax.py b/tests/test_organizations/object_syntax.py new file mode 100644 index 000000000..35437be62 --- /dev/null +++ b/tests/test_organizations/object_syntax.py @@ -0,0 +1,24 @@ +""" +Temporary functions for checking object structures while specing out +models. This module will go away. +""" + +import yaml +import moto +from moto import organizations as orgs + + +# utils +print(orgs.utils.make_random_org_id()) +print(orgs.utils.make_random_root_id()) +print(orgs.utils.make_random_account_id()) +print(orgs.utils.make_random_create_account_id()) + +# models +my_org = orgs.models.FakeOrganization(feature_set = 'ALL') +print(yaml.dump(my_org._describe())) +#assert False + +my_account = orgs.models.FakeAccount(my_org, AccountName='blee01', Email='blee01@moto-example.org') +print(yaml.dump(my_account)) +#assert False diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py new file mode 100644 index 000000000..fa70f3306 --- /dev/null +++ b/tests/test_organizations/test_organizations_boto3.py @@ -0,0 +1,193 @@ +from __future__ import unicode_literals + +import boto3 +import botocore.exceptions +import sure # noqa +import yaml +import re +import datetime + +import moto +from moto import mock_organizations +from moto.organizations.models import ( + MASTER_ACCOUNT_ID, + MASTER_ACCOUNT_EMAIL, + ORGANIZATION_ARN_FORMAT, + MASTER_ACCOUNT_ARN_FORMAT, + ACCOUNT_ARN_FORMAT, +) +from .test_organizations_utils import ( + ORG_ID_REGEX, + ROOT_ID_REGEX, + ACCOUNT_ID_REGEX, + CREATE_ACCOUNT_STATUS_ID_REGEX, +) + +EMAIL_REGEX = "^.+@[a-zA-Z0-9-.]+.[a-zA-Z]{2,3}|[0-9]{1,3}$" + + +def validate_organization(response): + org = response['Organization'] + sorted(org.keys()).should.equal([ + 'Arn', + 'AvailablePolicyTypes', + 'FeatureSet', + 'Id', + 'MasterAccountArn', + 'MasterAccountEmail', + 'MasterAccountId', + ]) + org['Id'].should.match(ORG_ID_REGEX) + org['MasterAccountId'].should.equal(MASTER_ACCOUNT_ID) + org['MasterAccountArn'].should.equal(MASTER_ACCOUNT_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + )) + org['Arn'].should.equal(ORGANIZATION_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + )) + org['MasterAccountEmail'].should.equal(MASTER_ACCOUNT_EMAIL) + org['FeatureSet'].should.be.within(['ALL', 'CONSOLIDATED_BILLING']) + org['AvailablePolicyTypes'].should.equal([{ + 'Type': 'SERVICE_CONTROL_POLICY', + 'Status': 'ENABLED' + }]) + # + #'Organization': { + # 'Id': 'string', + # 'Arn': 'string', + # 'FeatureSet': 'ALL'|'CONSOLIDATED_BILLING', + # 'MasterAccountArn': 'string', + # 'MasterAccountId': 'string', + # 'MasterAccountEmail': 'string', + # 'AvailablePolicyTypes': [ + # { + # 'Type': 'SERVICE_CONTROL_POLICY', + # 'Status': 'ENABLED'|'PENDING_ENABLE'|'PENDING_DISABLE' + # }, + # ] + #} + +def validate_account(org, account): + sorted(account.keys()).should.equal([ + 'Arn', + 'Email', + 'Id', + 'JoinedMethod', + 'JoinedTimestamp', + 'Name', + 'Status', + ]) + account['Id'].should.match(ACCOUNT_ID_REGEX) + account['Arn'].should.equal(ACCOUNT_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + account['Id'], + )) + account['Email'].should.match(EMAIL_REGEX) + account['JoinedMethod'].should.be.within(['INVITED', 'CREATED']) + account['Status'].should.be.within(['ACTIVE', 'SUSPENDED']) + account['Name'].should.be.a(str) + account['JoinedTimestamp'].should.be.a(datetime.datetime) + #'Account': { + # 'Id': 'string', + # 'Arn': 'string', + # 'Email': 'string', + # 'Name': 'string', + # 'Status': 'ACTIVE'|'SUSPENDED', + # 'JoinedMethod': 'INVITED'|'CREATED', + # 'JoinedTimestamp': datetime(2015, 1, 1) + #} + +def validate_create_account_status(create_status): + sorted(create_status.keys()).should.equal([ + 'AccountId', + 'AccountName', + 'CompletedTimestamp', + 'Id', + 'RequestedTimestamp', + 'State', + ]) + create_status['Id'].should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) + create_status['AccountId'].should.match(ACCOUNT_ID_REGEX) + create_status['AccountName'].should.be.a(str) + create_status['State'].should.equal('SUCCEEDED') + create_status['RequestedTimestamp'].should.be.a(datetime.datetime) + create_status['CompletedTimestamp'].should.be.a(datetime.datetime) + #'CreateAccountStatus': { + # 'Id': 'string', + # 'AccountName': 'string', + # 'State': 'IN_PROGRESS'|'SUCCEEDED'|'FAILED', + # 'RequestedTimestamp': datetime(2015, 1, 1), + # 'CompletedTimestamp': datetime(2015, 1, 1), + # 'AccountId': 'string', + # 'FailureReason': 'ACCOUNT_LIMIT_EXCEEDED'|'EMAIL_ALREADY_EXISTS'|'INVALID_ADDRESS'|'INVALID_EMAIL'|'CONCURRENT_ACCOUNT_MODIFICATION'|'INTERNAL_FAILURE' + #} + +@mock_organizations +def test_create_organization(): + client = boto3.client('organizations', region_name='us-east-1') + response = client.create_organization(FeatureSet='ALL') + #print(yaml.dump(response)) + validate_organization(response) + response['Organization']['FeatureSet'].should.equal('ALL') + #assert False + +@mock_organizations +def test_describe_organization(): + client = boto3.client('organizations', region_name='us-east-1') + client.create_organization(FeatureSet='ALL') + response = client.describe_organization() + #print(yaml.dump(response)) + validate_organization(response) + #assert False + + +mockname = 'mock-account' +mockdomain = 'moto-example.org' +mockemail = '@'.join([mockname, mockdomain]) + +@mock_organizations +def test_create_account(): + client = boto3.client('organizations', region_name='us-east-1') + client.create_organization(FeatureSet='ALL') + create_status = client.create_account( + AccountName=mockname, Email=mockemail)['CreateAccountStatus'] + #print(yaml.dump(create_status, default_flow_style=False)) + validate_create_account_status(create_status) + create_status['AccountName'].should.equal(mockname) + #assert False + +@mock_organizations +def test_describe_account(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + account_id = client.create_account( + AccountName=mockname, Email=mockemail)['CreateAccountStatus']['AccountId'] + response = client.describe_account(AccountId=account_id) + #print(yaml.dump(response, default_flow_style=False)) + validate_account(org, response['Account']) + response['Account']['Name'].should.equal(mockname) + response['Account']['Email'].should.equal(mockemail) + #assert False + +@mock_organizations +def test_list_accounts(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + for i in range(5): + name = mockname + str(i) + email = name + '@' + mockdomain + client.create_account(AccountName=name, Email=email) + response = client.list_accounts() + #print(yaml.dump(response, default_flow_style=False)) + response.should.have.key('Accounts') + accounts = response['Accounts'] + len(accounts).should.equal(5) + for account in accounts: + validate_account(org, account) + accounts[3]['Name'].should.equal(mockname + '3') + accounts[2]['Email'].should.equal(mockname + '2' + '@' + mockdomain) + #assert False + diff --git a/tests/test_organizations/test_organizations_utils.py b/tests/test_organizations/test_organizations_utils.py new file mode 100644 index 000000000..8144c327f --- /dev/null +++ b/tests/test_organizations/test_organizations_utils.py @@ -0,0 +1,26 @@ +from __future__ import unicode_literals + +import sure # noqa +import moto +from moto.organizations import utils + +ORG_ID_REGEX = r'o-[a-z0-9]{%s}' % utils.ORG_ID_SIZE +ROOT_ID_REGEX = r'r-[a-z0-9]{%s}' % utils.ROOT_ID_SIZE +ACCOUNT_ID_REGEX = r'[0-9]{%s}' % utils.ACCOUNT_ID_SIZE +CREATE_ACCOUNT_STATUS_ID_REGEX = r'car-[a-z0-9]{%s}' % utils.CREATE_ACCOUNT_STATUS_ID_SIZE + +def test_make_random_org_id(): + org_id = utils.make_random_org_id() + org_id.should.match(ORG_ID_REGEX) + +def test_make_random_root_id(): + org_id = utils.make_random_root_id() + org_id.should.match(ROOT_ID_REGEX) + +def test_make_random_account_id(): + account_id = utils.make_random_account_id() + account_id.should.match(ACCOUNT_ID_REGEX) + +def test_make_random_create_account_status_id(): + create_account_status_id = utils.make_random_create_account_status_id() + create_account_status_id.should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) From f20898da0ef6c9a8c46866fb596e713054afc349 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sat, 14 Jul 2018 12:28:19 -0700 Subject: [PATCH 02/16] add info on organizations support to docs --- IMPLEMENTATION_COVERAGE.md | 12 ++++++------ README.md | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 411f55a8b..837850ac6 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3147,22 +3147,22 @@ - [ ] update_server - [ ] update_server_engine_attributes -## organizations - 0% implemented +## organizations - 8% implemented - [ ] accept_handshake - [ ] attach_policy - [ ] cancel_handshake -- [ ] create_account -- [ ] create_organization +- [X] create_account +- [X] create_organization - [ ] create_organizational_unit - [ ] create_policy - [ ] decline_handshake - [ ] delete_organization - [ ] delete_organizational_unit - [ ] delete_policy -- [ ] describe_account +- [X] describe_account - [ ] describe_create_account_status - [ ] describe_handshake -- [ ] describe_organization +- [X] describe_organization - [ ] describe_organizational_unit - [ ] describe_policy - [ ] detach_policy @@ -3173,7 +3173,7 @@ - [ ] enable_policy_type - [ ] invite_account_to_organization - [ ] leave_organization -- [ ] list_accounts +- [X] list_accounts - [ ] list_accounts_for_parent - [ ] list_aws_service_access_for_organization - [ ] list_children diff --git a/README.md b/README.md index a6926a58f..e4fcb6508 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | KMS | @mock_kms | basic endpoints done | |------------------------------------------------------------------------------| +| Organizations | @mock_organizations | some endpoints done | +|------------------------------------------------------------------------------| | Polly | @mock_polly | all endpoints done | |------------------------------------------------------------------------------| | RDS | @mock_rds | core endpoints done | From c40d2be646884e333098bdb5fb1941f8f327cba4 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sat, 14 Jul 2018 13:23:15 -0700 Subject: [PATCH 03/16] organizations: clean up for flake8 --- moto/organizations/models.py | 8 +-- moto/organizations/responses.py | 1 - moto/organizations/utils.py | 11 ++-- tests/test_organizations/object_syntax.py | 3 +- .../test_organizations_boto3.py | 53 +++++-------------- .../test_organizations_utils.py | 5 +- 6 files changed, 26 insertions(+), 55 deletions(-) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 50212af29..e1d59c1da 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import datetime -import time from moto.core import BaseBackend, BaseModel from moto.core.utils import unix_time @@ -25,7 +24,7 @@ class FakeOrganization(BaseModel): 'Type': 'SERVICE_CONTROL_POLICY', 'Status': 'ENABLED' }] - + @property def arn(self): return ORGANIZATION_ARN_FORMAT.format(self.master_account_id, self.id) @@ -115,7 +114,7 @@ class OrganizationsBackend(BaseBackend): return new_account.create_account_status def describe_account(self, **kwargs): - account = [account for account in self.accounts + account = [account for account in self.accounts if account.account_id == kwargs['AccountId']][0] return account.describe() @@ -126,6 +125,3 @@ class OrganizationsBackend(BaseBackend): organizations_backend = OrganizationsBackend() - - - diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 10288dedb..7c8d45014 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -45,4 +45,3 @@ class OrganizationsResponse(BaseResponse): return json.dumps( self.organizations_backend.list_accounts() ) - diff --git a/moto/organizations/utils.py b/moto/organizations/utils.py index 5916dc5ea..1b111c1a7 100644 --- a/moto/organizations/utils.py +++ b/moto/organizations/utils.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import random import string -CHARSET=string.ascii_lowercase + string.digits +CHARSET = string.ascii_lowercase + string.digits ORG_ID_SIZE = 10 ROOT_ID_SIZE = 4 ACCOUNT_ID_SIZE = 12 @@ -11,24 +11,27 @@ CREATE_ACCOUNT_STATUS_ID_SIZE = 8 def make_random_org_id(): - # The regex pattern for an organization ID string requires "o-" + # The regex pattern for an organization ID string requires "o-" # followed by from 10 to 32 lower-case letters or digits. # e.g. 'o-vipjnq5z86' return 'o-' + ''.join(random.choice(CHARSET) for x in range(ORG_ID_SIZE)) + def make_random_root_id(): - # The regex pattern for a root ID string requires "r-" followed by + # The regex pattern for a root ID string requires "r-" followed by # from 4 to 32 lower-case letters or digits. # e.g. 'r-3zwx' return 'r-' + ''.join(random.choice(CHARSET) for x in range(ROOT_ID_SIZE)) + def make_random_account_id(): # The regex pattern for an account ID string requires exactly 12 digits. # e.g. '488633172133' return ''.join([random.choice(string.digits) for n in range(ACCOUNT_ID_SIZE)]) + def make_random_create_account_status_id(): - # The regex pattern for an create account request ID string requires + # The regex pattern for an create account request ID string requires # "car-" followed by from 8 to 32 lower-case letters or digits. # e.g. 'car-35gxzwrp' return 'car-' + ''.join(random.choice(CHARSET) for x in range(CREATE_ACCOUNT_STATUS_ID_SIZE)) diff --git a/tests/test_organizations/object_syntax.py b/tests/test_organizations/object_syntax.py index 35437be62..2779d1d07 100644 --- a/tests/test_organizations/object_syntax.py +++ b/tests/test_organizations/object_syntax.py @@ -4,7 +4,6 @@ models. This module will go away. """ import yaml -import moto from moto import organizations as orgs @@ -15,7 +14,7 @@ print(orgs.utils.make_random_account_id()) print(orgs.utils.make_random_create_account_id()) # models -my_org = orgs.models.FakeOrganization(feature_set = 'ALL') +my_org = orgs.models.FakeOrganization(feature_set='ALL') print(yaml.dump(my_org._describe())) #assert False diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index fa70f3306..7ae8d9577 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -1,13 +1,9 @@ from __future__ import unicode_literals import boto3 -import botocore.exceptions import sure # noqa -import yaml -import re import datetime -import moto from moto import mock_organizations from moto.organizations.models import ( MASTER_ACCOUNT_ID, @@ -53,21 +49,7 @@ def validate_organization(response): 'Type': 'SERVICE_CONTROL_POLICY', 'Status': 'ENABLED' }]) - # - #'Organization': { - # 'Id': 'string', - # 'Arn': 'string', - # 'FeatureSet': 'ALL'|'CONSOLIDATED_BILLING', - # 'MasterAccountArn': 'string', - # 'MasterAccountId': 'string', - # 'MasterAccountEmail': 'string', - # 'AvailablePolicyTypes': [ - # { - # 'Type': 'SERVICE_CONTROL_POLICY', - # 'Status': 'ENABLED'|'PENDING_ENABLE'|'PENDING_DISABLE' - # }, - # ] - #} + def validate_account(org, account): sorted(account.keys()).should.equal([ @@ -84,21 +66,13 @@ def validate_account(org, account): org['MasterAccountId'], org['Id'], account['Id'], - )) + )) account['Email'].should.match(EMAIL_REGEX) account['JoinedMethod'].should.be.within(['INVITED', 'CREATED']) account['Status'].should.be.within(['ACTIVE', 'SUSPENDED']) account['Name'].should.be.a(str) account['JoinedTimestamp'].should.be.a(datetime.datetime) - #'Account': { - # 'Id': 'string', - # 'Arn': 'string', - # 'Email': 'string', - # 'Name': 'string', - # 'Status': 'ACTIVE'|'SUSPENDED', - # 'JoinedMethod': 'INVITED'|'CREATED', - # 'JoinedTimestamp': datetime(2015, 1, 1) - #} + def validate_create_account_status(create_status): sorted(create_status.keys()).should.equal([ @@ -115,15 +89,7 @@ def validate_create_account_status(create_status): create_status['State'].should.equal('SUCCEEDED') create_status['RequestedTimestamp'].should.be.a(datetime.datetime) create_status['CompletedTimestamp'].should.be.a(datetime.datetime) - #'CreateAccountStatus': { - # 'Id': 'string', - # 'AccountName': 'string', - # 'State': 'IN_PROGRESS'|'SUCCEEDED'|'FAILED', - # 'RequestedTimestamp': datetime(2015, 1, 1), - # 'CompletedTimestamp': datetime(2015, 1, 1), - # 'AccountId': 'string', - # 'FailureReason': 'ACCOUNT_LIMIT_EXCEEDED'|'EMAIL_ALREADY_EXISTS'|'INVALID_ADDRESS'|'INVALID_EMAIL'|'CONCURRENT_ACCOUNT_MODIFICATION'|'INTERNAL_FAILURE' - #} + @mock_organizations def test_create_organization(): @@ -134,6 +100,7 @@ def test_create_organization(): response['Organization']['FeatureSet'].should.equal('ALL') #assert False + @mock_organizations def test_describe_organization(): client = boto3.client('organizations', region_name='us-east-1') @@ -148,23 +115,27 @@ mockname = 'mock-account' mockdomain = 'moto-example.org' mockemail = '@'.join([mockname, mockdomain]) + @mock_organizations def test_create_account(): client = boto3.client('organizations', region_name='us-east-1') client.create_organization(FeatureSet='ALL') create_status = client.create_account( - AccountName=mockname, Email=mockemail)['CreateAccountStatus'] + AccountName=mockname, Email=mockemail + )['CreateAccountStatus'] #print(yaml.dump(create_status, default_flow_style=False)) validate_create_account_status(create_status) create_status['AccountName'].should.equal(mockname) #assert False + @mock_organizations def test_describe_account(): client = boto3.client('organizations', region_name='us-east-1') org = client.create_organization(FeatureSet='ALL')['Organization'] account_id = client.create_account( - AccountName=mockname, Email=mockemail)['CreateAccountStatus']['AccountId'] + AccountName=mockname, Email=mockemail + )['CreateAccountStatus']['AccountId'] response = client.describe_account(AccountId=account_id) #print(yaml.dump(response, default_flow_style=False)) validate_account(org, response['Account']) @@ -172,6 +143,7 @@ def test_describe_account(): response['Account']['Email'].should.equal(mockemail) #assert False + @mock_organizations def test_list_accounts(): client = boto3.client('organizations', region_name='us-east-1') @@ -190,4 +162,3 @@ def test_list_accounts(): accounts[3]['Name'].should.equal(mockname + '3') accounts[2]['Email'].should.equal(mockname + '2' + '@' + mockdomain) #assert False - diff --git a/tests/test_organizations/test_organizations_utils.py b/tests/test_organizations/test_organizations_utils.py index 8144c327f..3e29d5cb0 100644 --- a/tests/test_organizations/test_organizations_utils.py +++ b/tests/test_organizations/test_organizations_utils.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import sure # noqa -import moto from moto.organizations import utils ORG_ID_REGEX = r'o-[a-z0-9]{%s}' % utils.ORG_ID_SIZE @@ -9,18 +8,22 @@ ROOT_ID_REGEX = r'r-[a-z0-9]{%s}' % utils.ROOT_ID_SIZE ACCOUNT_ID_REGEX = r'[0-9]{%s}' % utils.ACCOUNT_ID_SIZE CREATE_ACCOUNT_STATUS_ID_REGEX = r'car-[a-z0-9]{%s}' % utils.CREATE_ACCOUNT_STATUS_ID_SIZE + def test_make_random_org_id(): org_id = utils.make_random_org_id() org_id.should.match(ORG_ID_REGEX) + def test_make_random_root_id(): org_id = utils.make_random_root_id() org_id.should.match(ROOT_ID_REGEX) + def test_make_random_account_id(): account_id = utils.make_random_account_id() account_id.should.match(ACCOUNT_ID_REGEX) + def test_make_random_create_account_status_id(): create_account_status_id = utils.make_random_create_account_status_id() create_account_status_id.should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) From 6c0c6148f15b7a9133a0c9cce190e6870f787d6e Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sun, 15 Jul 2018 10:31:16 -0700 Subject: [PATCH 04/16] organizations: add endpoint list_roots --- moto/organizations/models.py | 70 +++++++++++++++++-- moto/organizations/responses.py | 5 ++ moto/organizations/utils.py | 14 ++++ tests/test_organizations/object_syntax.py | 6 +- .../test_organizations_boto3.py | 24 +++++++ .../test_organizations_utils.py | 11 ++- 6 files changed, 122 insertions(+), 8 deletions(-) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index e1d59c1da..265654030 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -11,7 +11,7 @@ MASTER_ACCOUNT_EMAIL = 'fakeorg@moto-example.com' ORGANIZATION_ARN_FORMAT = 'arn:aws:organizations::{0}:organization/{1}' MASTER_ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{0}' ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{2}' - +ROOT_ARN_FORMAT = 'arn:aws:organizations::{0}:root/{1}/{2}' class FakeOrganization(BaseModel): @@ -33,7 +33,7 @@ class FakeOrganization(BaseModel): def master_account_arn(self): return MASTER_ACCOUNT_ARN_FORMAT.format(self.master_account_id, self.id) - def _describe(self): + def describe(self): return { 'Organization': { 'Id': self.id, @@ -95,18 +95,80 @@ class FakeAccount(BaseModel): } +class FakeOrganizationalUnit(BaseModel): + + def __init__(self, organization, **kwargs): + self.organization_id = organization.id + self.master_account_id = organization.master_account_id + self.id = utils.make_random_ou_id() + self.name = kwargs['Name'] + + @property + def arn(self): + return OU_ARN_FORMAT.format( + self.master_account_id, + self.organization_id, + self.id + ) + + def describe(self): + return { + 'OrganizationalUnit': { + 'Id': self.id, + 'Arn': self.arn, + 'Name': self.name, + } + } + + +class FakeRoot(BaseModel): + + def __init__(self, organization, **kwargs): + self.organization_id = organization.id + self.master_account_id = organization.master_account_id + self.id = utils.make_random_root_id() + self.name = 'Root' + self.policy_types = [{ + 'Type': 'SERVICE_CONTROL_POLICY', + 'Status': 'ENABLED' + }] + + @property + def arn(self): + return ROOT_ARN_FORMAT.format( + self.master_account_id, + self.organization_id, + self.id + ) + + def describe(self): + return { + 'Id': self.id, + 'Arn': self.arn, + 'Name': self.name, + 'PolicyTypes': self.policy_types + } + + class OrganizationsBackend(BaseBackend): def __init__(self): self.org = None self.accounts = [] + self.roots = [] def create_organization(self, **kwargs): self.org = FakeOrganization(kwargs['FeatureSet']) - return self.org._describe() + self.roots.append(FakeRoot(self.org)) + return self.org.describe() def describe_organization(self): - return self.org._describe() + return self.org.describe() + + def list_roots(self): + return dict( + Roots=[root.describe() for root in self.roots] + ) def create_account(self, **kwargs): new_account = FakeAccount(self.org, **kwargs) diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 7c8d45014..1804a3fcf 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -31,6 +31,11 @@ class OrganizationsResponse(BaseResponse): self.organizations_backend.describe_organization() ) + def list_roots(self): + return json.dumps( + self.organizations_backend.list_roots() + ) + def create_account(self): return json.dumps( self.organizations_backend.create_account(**self.request_params) diff --git a/moto/organizations/utils.py b/moto/organizations/utils.py index 1b111c1a7..c7e5c71cd 100644 --- a/moto/organizations/utils.py +++ b/moto/organizations/utils.py @@ -7,6 +7,7 @@ CHARSET = string.ascii_lowercase + string.digits ORG_ID_SIZE = 10 ROOT_ID_SIZE = 4 ACCOUNT_ID_SIZE = 12 +OU_ID_SUFFIX_SIZE = 8 CREATE_ACCOUNT_STATUS_ID_SIZE = 8 @@ -24,6 +25,19 @@ def make_random_root_id(): return 'r-' + ''.join(random.choice(CHARSET) for x in range(ROOT_ID_SIZE)) +def make_random_ou_id(root_id): + # The regex pattern for an organizational unit ID string requires "ou-" + # followed by from 4 to 32 lower-case letters or digits (the ID of the root + # that contains the OU) followed by a second "-" dash and from 8 to 32 + # additional lower-case letters or digits. + # e.g. ou-g8sd-5oe3bjaw + return '-'.join([ + 'ou', + root_id.partition('-')[2], + ''.join(random.choice(CHARSET) for x in range(OU_ID_SUFFIX_SIZE)), + ]) + + def make_random_account_id(): # The regex pattern for an account ID string requires exactly 12 digits. # e.g. '488633172133' diff --git a/tests/test_organizations/object_syntax.py b/tests/test_organizations/object_syntax.py index 2779d1d07..3fb86b9d5 100644 --- a/tests/test_organizations/object_syntax.py +++ b/tests/test_organizations/object_syntax.py @@ -9,9 +9,11 @@ from moto import organizations as orgs # utils print(orgs.utils.make_random_org_id()) -print(orgs.utils.make_random_root_id()) +root_id = orgs.utils.make_random_root_id() +print(root_id) +print(orgs.utils.make_random_ou_id(root_id)) print(orgs.utils.make_random_account_id()) -print(orgs.utils.make_random_create_account_id()) +print(orgs.utils.make_random_create_account_status_id()) # models my_org = orgs.models.FakeOrganization(feature_set='ALL') diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 7ae8d9577..f4dc3b445 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import boto3 import sure # noqa import datetime +import yaml from moto import mock_organizations from moto.organizations.models import ( @@ -11,6 +12,7 @@ from moto.organizations.models import ( ORGANIZATION_ARN_FORMAT, MASTER_ACCOUNT_ARN_FORMAT, ACCOUNT_ARN_FORMAT, + ROOT_ARN_FORMAT, ) from .test_organizations_utils import ( ORG_ID_REGEX, @@ -111,6 +113,28 @@ def test_describe_organization(): #assert False +@mock_organizations +def test_list_roots(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + response = client.list_roots() + #print(yaml.dump(response, default_flow_style=False)) + response.should.have.key('Roots').should.be.a(list) + response['Roots'].should_not.be.empty + root = response['Roots'][0] + root.should.have.key('Id').should.match(ROOT_ID_REGEX) + root.should.have.key('Arn').should.equal(ROOT_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + root['Id'], + )) + root.should.have.key('Name').should.be.a(str) + root.should.have.key('PolicyTypes').should.be.a(list) + root['PolicyTypes'][0].should.have.key('Type').should.equal('SERVICE_CONTROL_POLICY') + root['PolicyTypes'][0].should.have.key('Status').should.equal('ENABLED') + #assert False + + mockname = 'mock-account' mockdomain = 'moto-example.org' mockemail = '@'.join([mockname, mockdomain]) diff --git a/tests/test_organizations/test_organizations_utils.py b/tests/test_organizations/test_organizations_utils.py index 3e29d5cb0..d27201446 100644 --- a/tests/test_organizations/test_organizations_utils.py +++ b/tests/test_organizations/test_organizations_utils.py @@ -5,6 +5,7 @@ from moto.organizations import utils ORG_ID_REGEX = r'o-[a-z0-9]{%s}' % utils.ORG_ID_SIZE ROOT_ID_REGEX = r'r-[a-z0-9]{%s}' % utils.ROOT_ID_SIZE +OU_ID_REGEX = r'ou-[a-z0-9]{%s}-[a-z0-9]{%s}' % (utils.ROOT_ID_SIZE, utils.OU_ID_SUFFIX_SIZE) ACCOUNT_ID_REGEX = r'[0-9]{%s}' % utils.ACCOUNT_ID_SIZE CREATE_ACCOUNT_STATUS_ID_REGEX = r'car-[a-z0-9]{%s}' % utils.CREATE_ACCOUNT_STATUS_ID_SIZE @@ -15,8 +16,14 @@ def test_make_random_org_id(): def test_make_random_root_id(): - org_id = utils.make_random_root_id() - org_id.should.match(ROOT_ID_REGEX) + root_id = utils.make_random_root_id() + root_id.should.match(ROOT_ID_REGEX) + + +def test_make_random_ou_id(): + root_id = utils.make_random_root_id() + ou_id = utils.make_random_ou_id(root_id) + ou_id.should.match(OU_ID_REGEX) def test_make_random_account_id(): From beebb9abc871f826597e12ba29bcdaa2cd78f4af Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sun, 15 Jul 2018 11:49:26 -0700 Subject: [PATCH 05/16] organizations: add 2 more endpoints create_organizational_unit describe_organizational_unit --- IMPLEMENTATION_COVERAGE.md | 8 +-- moto/organizations/models.py | 72 +++++++++++-------- moto/organizations/responses.py | 10 +++ .../test_organizations_boto3.py | 49 +++++++++++++ 4 files changed, 107 insertions(+), 32 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 837850ac6..7b4bc5e4d 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3147,13 +3147,13 @@ - [ ] update_server - [ ] update_server_engine_attributes -## organizations - 8% implemented +## organizations - 19% implemented - [ ] accept_handshake - [ ] attach_policy - [ ] cancel_handshake - [X] create_account - [X] create_organization -- [ ] create_organizational_unit +- [X] create_organizational_unit - [ ] create_policy - [ ] decline_handshake - [ ] delete_organization @@ -3163,7 +3163,7 @@ - [ ] describe_create_account_status - [ ] describe_handshake - [X] describe_organization -- [ ] describe_organizational_unit +- [X] describe_organizational_unit - [ ] describe_policy - [ ] detach_policy - [ ] disable_aws_service_access @@ -3184,7 +3184,7 @@ - [ ] list_parents - [ ] list_policies - [ ] list_policies_for_target -- [ ] list_roots +- [X] list_roots - [ ] list_targets_for_policy - [ ] move_account - [ ] remove_account_from_organization diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 265654030..461072ce3 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -12,6 +12,7 @@ ORGANIZATION_ARN_FORMAT = 'arn:aws:organizations::{0}:organization/{1}' MASTER_ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{0}' ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{2}' ROOT_ARN_FORMAT = 'arn:aws:organizations::{0}:root/{1}/{2}' +OU_ARN_FORMAT = 'arn:aws:organizations::{0}:ou/{1}/{2}' class FakeOrganization(BaseModel): @@ -95,32 +96,6 @@ class FakeAccount(BaseModel): } -class FakeOrganizationalUnit(BaseModel): - - def __init__(self, organization, **kwargs): - self.organization_id = organization.id - self.master_account_id = organization.master_account_id - self.id = utils.make_random_ou_id() - self.name = kwargs['Name'] - - @property - def arn(self): - return OU_ARN_FORMAT.format( - self.master_account_id, - self.organization_id, - self.id - ) - - def describe(self): - return { - 'OrganizationalUnit': { - 'Id': self.id, - 'Arn': self.arn, - 'Name': self.name, - } - } - - class FakeRoot(BaseModel): def __init__(self, organization, **kwargs): @@ -150,12 +125,40 @@ class FakeRoot(BaseModel): } + +class FakeOrganizationalUnit(BaseModel): + + def __init__(self, organization, root_id, **kwargs): + self.organization_id = organization.id + self.master_account_id = organization.master_account_id + self.id = utils.make_random_ou_id(root_id) + self.name = kwargs['Name'] + self.parent_id = kwargs['ParentId'] + + @property + def arn(self): + return OU_ARN_FORMAT.format( + self.master_account_id, + self.organization_id, + self.id + ) + + def describe(self): + return { + 'OrganizationalUnit': { + 'Id': self.id, + 'Arn': self.arn, + 'Name': self.name, + } + } + class OrganizationsBackend(BaseBackend): def __init__(self): self.org = None self.accounts = [] self.roots = [] + self.ou = [] def create_organization(self, **kwargs): self.org = FakeOrganization(kwargs['FeatureSet']) @@ -170,14 +173,27 @@ class OrganizationsBackend(BaseBackend): Roots=[root.describe() for root in self.roots] ) + def create_organizational_unit(self, **kwargs): + new_ou = FakeOrganizationalUnit(self.org, self.roots[0].id, **kwargs) + self.ou.append(new_ou) + return new_ou.describe() + + def describe_organizational_unit(self, **kwargs): + ou = [ + ou for ou in self.ou if ou.id == kwargs['OrganizationalUnitId'] + ].pop(0) + return ou.describe() + def create_account(self, **kwargs): new_account = FakeAccount(self.org, **kwargs) self.accounts.append(new_account) return new_account.create_account_status def describe_account(self, **kwargs): - account = [account for account in self.accounts - if account.account_id == kwargs['AccountId']][0] + account = [ + account for account in self.accounts + if account.account_id == kwargs['AccountId'] + ].pop(0) return account.describe() def list_accounts(self): diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 1804a3fcf..4f0643cf6 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -36,6 +36,16 @@ class OrganizationsResponse(BaseResponse): self.organizations_backend.list_roots() ) + def create_organizational_unit(self): + return json.dumps( + self.organizations_backend.create_organizational_unit(**self.request_params) + ) + + def describe_organizational_unit(self): + return json.dumps( + self.organizations_backend.describe_organizational_unit(**self.request_params) + ) + def create_account(self): return json.dumps( self.organizations_backend.create_account(**self.request_params) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index f4dc3b445..0e398bd43 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -13,10 +13,12 @@ from moto.organizations.models import ( MASTER_ACCOUNT_ARN_FORMAT, ACCOUNT_ARN_FORMAT, ROOT_ARN_FORMAT, + OU_ARN_FORMAT, ) from .test_organizations_utils import ( ORG_ID_REGEX, ROOT_ID_REGEX, + OU_ID_REGEX, ACCOUNT_ID_REGEX, CREATE_ACCOUNT_STATUS_ID_REGEX, ) @@ -53,6 +55,18 @@ def validate_organization(response): }]) +def validate_organizationa_unit(org, response): + response.should.have.key('OrganizationalUnit').should.be.a(dict) + ou = response['OrganizationalUnit'] + ou.should.have.key('Id').should.match(OU_ID_REGEX) + ou.should.have.key('Arn').should.equal(OU_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + ou['Id'], + )) + ou.should.have.key('Name').should.equal(ou_name) + + def validate_account(org, account): sorted(account.keys()).should.equal([ 'Arn', @@ -113,6 +127,9 @@ def test_describe_organization(): #assert False +# Organizational Units +ou_name = 'ou01' + @mock_organizations def test_list_roots(): client = boto3.client('organizations', region_name='us-east-1') @@ -135,6 +152,38 @@ def test_list_roots(): #assert False +@mock_organizations +def test_create_organizational_unit(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + response = client.create_organizational_unit( + ParentId=root_id, + Name=ou_name, + ) + #print(yaml.dump(response, default_flow_style=False)) + validate_organizationa_unit(org, response) + #assert False + + +@mock_organizations +def test_describe_organizational_unit(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + ou_id = client.create_organizational_unit( + ParentId=root_id, + Name=ou_name, + )['OrganizationalUnit']['Id'] + response = client.describe_organizational_unit( + OrganizationalUnitId=ou_id, + ) + print(yaml.dump(response, default_flow_style=False)) + validate_organizationa_unit(org, response) + #assert False + + +# Accounts mockname = 'mock-account' mockdomain = 'moto-example.org' mockemail = '@'.join([mockname, mockdomain]) From fc2447c6a49139ce9fd06bbc48aa23f3fd1bcae7 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sun, 15 Jul 2018 13:58:27 -0700 Subject: [PATCH 06/16] organiziaions: 2 new endpoints: list_organizational_units_for_parents list_parents --- IMPLEMENTATION_COVERAGE.md | 6 +- moto/organizations/models.py | 32 ++++++++++- moto/organizations/responses.py | 10 ++++ .../test_organizations_boto3.py | 57 ++++++++++++++++--- 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 7b4bc5e4d..84c574bc3 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3147,7 +3147,7 @@ - [ ] update_server - [ ] update_server_engine_attributes -## organizations - 19% implemented +## organizations - 20% implemented - [ ] accept_handshake - [ ] attach_policy - [ ] cancel_handshake @@ -3180,8 +3180,8 @@ - [ ] list_create_account_status - [ ] list_handshakes_for_account - [ ] list_handshakes_for_organization -- [ ] list_organizational_units_for_parent -- [ ] list_parents +- [X] list_organizational_units_for_parent +- [X] list_parents - [ ] list_policies - [ ] list_policies_for_target - [X] list_roots diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 461072ce3..b46c62798 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -14,6 +14,7 @@ ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{2}' ROOT_ARN_FORMAT = 'arn:aws:organizations::{0}:root/{1}/{2}' OU_ARN_FORMAT = 'arn:aws:organizations::{0}:ou/{1}/{2}' + class FakeOrganization(BaseModel): def __init__(self, feature_set): @@ -125,7 +126,6 @@ class FakeRoot(BaseModel): } - class FakeOrganizationalUnit(BaseModel): def __init__(self, organization, root_id, **kwargs): @@ -152,6 +152,7 @@ class FakeOrganizationalUnit(BaseModel): } } + class OrganizationsBackend(BaseBackend): def __init__(self): @@ -184,6 +185,35 @@ class OrganizationsBackend(BaseBackend): ].pop(0) return ou.describe() + def list_organizational_units_for_parent(self, **kwargs): + return dict( + OrganizationalUnits=[ + { + 'Id': ou.id, + 'Arn': ou.arn, + 'Name': ou.name, + } + for ou in self.ou + if ou.parent_id == kwargs['ParentId'] + ] + ) + + def list_parents(self, **kwargs): + parent_id = [ + ou.parent_id for ou in self.ou if ou.id == kwargs['ChildId'] + ].pop(0) + root_parents = [ + dict(Id=root.id, Type='ROOT') + for root in self.roots + if root.id == parent_id + ] + ou_parents = [ + dict(Id=ou.id, Type='ORGANIZATIONAL_UNIT') + for ou in self.ou + if ou.id == parent_id + ] + return dict(Parents=root_parents + ou_parents) + def create_account(self, **kwargs): new_account = FakeAccount(self.org, **kwargs) self.accounts.append(new_account) diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 4f0643cf6..9fdb73317 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -46,6 +46,16 @@ class OrganizationsResponse(BaseResponse): self.organizations_backend.describe_organizational_unit(**self.request_params) ) + def list_organizational_units_for_parent(self): + return json.dumps( + self.organizations_backend.list_organizational_units_for_parent(**self.request_params) + ) + + def list_parents(self): + return json.dumps( + self.organizations_backend.list_parents(**self.request_params) + ) + def create_account(self): return json.dumps( self.organizations_backend.create_account(**self.request_params) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 0e398bd43..8375ffbe3 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -55,7 +55,7 @@ def validate_organization(response): }]) -def validate_organizationa_unit(org, response): +def validate_organizational_unit(org, response): response.should.have.key('OrganizationalUnit').should.be.a(dict) ou = response['OrganizationalUnit'] ou.should.have.key('Id').should.match(OU_ID_REGEX) @@ -64,7 +64,7 @@ def validate_organizationa_unit(org, response): org['Id'], ou['Id'], )) - ou.should.have.key('Name').should.equal(ou_name) + ou.should.have.key('Name').should.be.a(str) def validate_account(org, account): @@ -128,7 +128,6 @@ def test_describe_organization(): # Organizational Units -ou_name = 'ou01' @mock_organizations def test_list_roots(): @@ -157,12 +156,14 @@ def test_create_organizational_unit(): client = boto3.client('organizations', region_name='us-east-1') org = client.create_organization(FeatureSet='ALL')['Organization'] root_id = client.list_roots()['Roots'][0]['Id'] + ou_name = 'ou01' response = client.create_organizational_unit( ParentId=root_id, Name=ou_name, ) #print(yaml.dump(response, default_flow_style=False)) - validate_organizationa_unit(org, response) + validate_organizational_unit(org, response) + response['OrganizationalUnit']['Name'].should.equal(ou_name) #assert False @@ -173,13 +174,51 @@ def test_describe_organizational_unit(): root_id = client.list_roots()['Roots'][0]['Id'] ou_id = client.create_organizational_unit( ParentId=root_id, - Name=ou_name, + Name='ou01', )['OrganizationalUnit']['Id'] - response = client.describe_organizational_unit( - OrganizationalUnitId=ou_id, - ) + response = client.describe_organizational_unit(OrganizationalUnitId=ou_id) print(yaml.dump(response, default_flow_style=False)) - validate_organizationa_unit(org, response) + validate_organizational_unit(org, response) + #assert False + + +@mock_organizations +def test_list_organizational_units_for_parent(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + client.create_organizational_unit(ParentId=root_id, Name='ou01') + client.create_organizational_unit(ParentId=root_id, Name='ou02') + client.create_organizational_unit(ParentId=root_id, Name='ou03') + response = client.list_organizational_units_for_parent(ParentId=root_id) + print(yaml.dump(response, default_flow_style=False)) + response.should.have.key('OrganizationalUnits').should.be.a(list) + for ou in response['OrganizationalUnits']: + validate_organizational_unit(org, dict(OrganizationalUnit=ou)) + #assert False + + +@mock_organizations +def test_list_parents(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + + ou01 = client.create_organizational_unit(ParentId=root_id, Name='ou01') + ou01_id = ou01['OrganizationalUnit']['Id'] + response01 = client.list_parents(ChildId=ou01_id) + #print(yaml.dump(response01, default_flow_style=False)) + response01.should.have.key('Parents').should.be.a(list) + response01['Parents'][0].should.have.key('Id').should.equal(root_id) + response01['Parents'][0].should.have.key('Type').should.equal('ROOT') + + ou02 = client.create_organizational_unit(ParentId=ou01_id, Name='ou02') + ou02_id = ou02['OrganizationalUnit']['Id'] + response02 = client.list_parents(ChildId=ou02_id) + #print(yaml.dump(response02, default_flow_style=False)) + response02.should.have.key('Parents').should.be.a(list) + response02['Parents'][0].should.have.key('Id').should.equal(ou01_id) + response02['Parents'][0].should.have.key('Type').should.equal('ORGANIZATIONAL_UNIT') #assert False From 009dcdb21a10399f7b4fb6f9465267de0e7324bb Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sun, 15 Jul 2018 15:25:34 -0700 Subject: [PATCH 07/16] organizations: and another 2 endpoints: list_accounts_for_parent move_account --- IMPLEMENTATION_COVERAGE.md | 6 ++-- moto/organizations/models.py | 25 +++++++++++-- moto/organizations/responses.py | 10 ++++++ .../test_organizations_boto3.py | 36 +++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 84c574bc3..e1943aa9a 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3147,7 +3147,7 @@ - [ ] update_server - [ ] update_server_engine_attributes -## organizations - 20% implemented +## organizations - 28% implemented - [ ] accept_handshake - [ ] attach_policy - [ ] cancel_handshake @@ -3174,7 +3174,7 @@ - [ ] invite_account_to_organization - [ ] leave_organization - [X] list_accounts -- [ ] list_accounts_for_parent +- [X] list_accounts_for_parent - [ ] list_aws_service_access_for_organization - [ ] list_children - [ ] list_create_account_status @@ -3186,7 +3186,7 @@ - [ ] list_policies_for_target - [X] list_roots - [ ] list_targets_for_policy -- [ ] move_account +- [X] move_account - [ ] remove_account_from_organization - [ ] update_organizational_unit - [ ] update_policy diff --git a/moto/organizations/models.py b/moto/organizations/models.py index b46c62798..bbcc1479b 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -51,7 +51,7 @@ class FakeOrganization(BaseModel): class FakeAccount(BaseModel): - def __init__(self, organization, **kwargs): + def __init__(self, organization, root_id, **kwargs): self.organization_id = organization.id self.master_account_id = organization.master_account_id self.create_account_status_id = utils.make_random_create_account_status_id() @@ -61,6 +61,7 @@ class FakeAccount(BaseModel): self.create_time = datetime.datetime.utcnow() self.status = 'ACTIVE' self.joined_method = 'CREATED' + self.parent_id = root_id @property def arn(self): @@ -215,7 +216,7 @@ class OrganizationsBackend(BaseBackend): return dict(Parents=root_parents + ou_parents) def create_account(self, **kwargs): - new_account = FakeAccount(self.org, **kwargs) + new_account = FakeAccount(self.org, self.roots[0].id, **kwargs) self.accounts.append(new_account) return new_account.create_account_status @@ -231,5 +232,25 @@ class OrganizationsBackend(BaseBackend): Accounts=[account.describe()['Account'] for account in self.accounts] ) + def list_accounts_for_parent(self, **kwargs): + return dict( + Accounts=[ + account.describe()['Account'] + for account in self.accounts + if account.parent_id == kwargs['ParentId'] + ] + ) + + def move_account(self, **kwargs): + new_parent_id = kwargs['DestinationParentId'] + all_parent_id = [parent.id for parent in self.roots + self.ou] + account = [ + account for account in self.accounts if account.account_id == kwargs['AccountId'] + ].pop(0) + assert new_parent_id in all_parent_id + assert account.parent_id == kwargs['SourceParentId'] + index = self.accounts.index(account) + self.accounts[index].parent_id = new_parent_id + organizations_backend = OrganizationsBackend() diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 9fdb73317..6684ae685 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -70,3 +70,13 @@ class OrganizationsResponse(BaseResponse): return json.dumps( self.organizations_backend.list_accounts() ) + + def list_accounts_for_parent(self): + return json.dumps( + self.organizations_backend.list_accounts_for_parent(**self.request_params) + ) + + def move_account(self): + return json.dumps( + self.organizations_backend.move_account(**self.request_params) + ) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 8375ffbe3..5355e2716 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -274,3 +274,39 @@ def test_list_accounts(): accounts[3]['Name'].should.equal(mockname + '3') accounts[2]['Email'].should.equal(mockname + '2' + '@' + mockdomain) #assert False + + +@mock_organizations +def test_list_accounts_for_parent(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + account_id = client.create_account( + AccountName=mockname, + Email=mockemail, + )['CreateAccountStatus']['AccountId'] + response = client.list_accounts_for_parent(ParentId=root_id) + #print(yaml.dump(response, default_flow_style=False)) + account_id.should.be.within([account['Id'] for account in response['Accounts']]) + #assert False + + +@mock_organizations +def test_move_account(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + account_id = client.create_account( + AccountName=mockname, Email=mockemail + )['CreateAccountStatus']['AccountId'] + ou01 = client.create_organizational_unit(ParentId=root_id, Name='ou01') + ou01_id = ou01['OrganizationalUnit']['Id'] + client.move_account( + AccountId=account_id, + SourceParentId=root_id, + DestinationParentId=ou01_id, + ) + response = client.list_accounts_for_parent(ParentId=ou01_id) + #print(yaml.dump(response, default_flow_style=False)) + account_id.should.be.within([account['Id'] for account in response['Accounts']]) + #assert False From 9b5c6c4f0f04392ab40df7404020fd70340e7347 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sun, 15 Jul 2018 18:48:56 -0700 Subject: [PATCH 08/16] organizations.model.FakeAccount: rename attributes: account_id -> id account_name -> name --- moto/organizations/models.py | 71 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index bbcc1479b..8a00918d1 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -55,8 +55,8 @@ class FakeAccount(BaseModel): self.organization_id = organization.id self.master_account_id = organization.master_account_id self.create_account_status_id = utils.make_random_create_account_status_id() - self.account_id = utils.make_random_account_id() - self.account_name = kwargs['AccountName'] + self.id = utils.make_random_account_id() + self.name = kwargs['AccountName'] self.email = kwargs['Email'] self.create_time = datetime.datetime.utcnow() self.status = 'ACTIVE' @@ -68,7 +68,7 @@ class FakeAccount(BaseModel): return ACCOUNT_ARN_FORMAT.format( self.master_account_id, self.organization_id, - self.account_id + self.id ) @property @@ -76,21 +76,21 @@ class FakeAccount(BaseModel): return { 'CreateAccountStatus': { 'Id': self.create_account_status_id, - 'AccountName': self.account_name, + 'AccountName': self.name, 'State': 'SUCCEEDED', 'RequestedTimestamp': unix_time(self.create_time), 'CompletedTimestamp': unix_time(self.create_time), - 'AccountId': self.account_id, + 'AccountId': self.id, } } def describe(self): return { 'Account': { - 'Id': self.account_id, + 'Id': self.id, 'Arn': self.arn, 'Email': self.email, - 'Name': self.account_name, + 'Name': self.name, 'Status': self.status, 'JoinedMethod': self.joined_method, 'JoinedTimestamp': unix_time(self.create_time), @@ -98,6 +98,33 @@ class FakeAccount(BaseModel): } +class FakeOrganizationalUnit(BaseModel): + + def __init__(self, organization, root_id, **kwargs): + self.organization_id = organization.id + self.master_account_id = organization.master_account_id + self.id = utils.make_random_ou_id(root_id) + self.name = kwargs['Name'] + self.parent_id = kwargs['ParentId'] + + @property + def arn(self): + return OU_ARN_FORMAT.format( + self.master_account_id, + self.organization_id, + self.id + ) + + def describe(self): + return { + 'OrganizationalUnit': { + 'Id': self.id, + 'Arn': self.arn, + 'Name': self.name, + } + } + + class FakeRoot(BaseModel): def __init__(self, organization, **kwargs): @@ -127,32 +154,6 @@ class FakeRoot(BaseModel): } -class FakeOrganizationalUnit(BaseModel): - - def __init__(self, organization, root_id, **kwargs): - self.organization_id = organization.id - self.master_account_id = organization.master_account_id - self.id = utils.make_random_ou_id(root_id) - self.name = kwargs['Name'] - self.parent_id = kwargs['ParentId'] - - @property - def arn(self): - return OU_ARN_FORMAT.format( - self.master_account_id, - self.organization_id, - self.id - ) - - def describe(self): - return { - 'OrganizationalUnit': { - 'Id': self.id, - 'Arn': self.arn, - 'Name': self.name, - } - } - class OrganizationsBackend(BaseBackend): @@ -223,7 +224,7 @@ class OrganizationsBackend(BaseBackend): def describe_account(self, **kwargs): account = [ account for account in self.accounts - if account.account_id == kwargs['AccountId'] + if account.id == kwargs['AccountId'] ].pop(0) return account.describe() @@ -245,7 +246,7 @@ class OrganizationsBackend(BaseBackend): new_parent_id = kwargs['DestinationParentId'] all_parent_id = [parent.id for parent in self.roots + self.ou] account = [ - account for account in self.accounts if account.account_id == kwargs['AccountId'] + account for account in self.accounts if account.id == kwargs['AccountId'] ].pop(0) assert new_parent_id in all_parent_id assert account.parent_id == kwargs['SourceParentId'] From 30a9aa33e55e35070eb2468c0d377eb79e7a14e3 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sun, 15 Jul 2018 20:39:13 -0700 Subject: [PATCH 09/16] organizations: endpoint list_parents now support account_id param refactered classes: FakeRoot inherits from FakeOrganizationsUnit add root_id attribute to class FakeOrganization dropped 'roots' attribute from class OrganizationaBackend --- moto/organizations/models.py | 84 ++++++++++--------- .../test_organizations_boto3.py | 79 +++++++++++------ 2 files changed, 98 insertions(+), 65 deletions(-) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 8a00918d1..609ce3831 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import datetime +import re from moto.core import BaseBackend, BaseModel from moto.core.utils import unix_time @@ -19,6 +20,7 @@ class FakeOrganization(BaseModel): def __init__(self, feature_set): self.id = utils.make_random_org_id() + self.root_id = utils.make_random_root_id() self.feature_set = feature_set self.master_account_id = MASTER_ACCOUNT_ID self.master_account_email = MASTER_ACCOUNT_EMAIL @@ -51,7 +53,7 @@ class FakeOrganization(BaseModel): class FakeAccount(BaseModel): - def __init__(self, organization, root_id, **kwargs): + def __init__(self, organization, **kwargs): self.organization_id = organization.id self.master_account_id = organization.master_account_id self.create_account_status_id = utils.make_random_create_account_status_id() @@ -61,7 +63,7 @@ class FakeAccount(BaseModel): self.create_time = datetime.datetime.utcnow() self.status = 'ACTIVE' self.joined_method = 'CREATED' - self.parent_id = root_id + self.parent_id = organization.root_id @property def arn(self): @@ -100,16 +102,18 @@ class FakeAccount(BaseModel): class FakeOrganizationalUnit(BaseModel): - def __init__(self, organization, root_id, **kwargs): + def __init__(self, organization, **kwargs): + self.type = 'ORGANIZATIONAL_UNIT' self.organization_id = organization.id self.master_account_id = organization.master_account_id - self.id = utils.make_random_ou_id(root_id) - self.name = kwargs['Name'] - self.parent_id = kwargs['ParentId'] + self.id = utils.make_random_ou_id(organization.root_id) + self.name = kwargs.get('Name') + self.parent_id = kwargs.get('ParentId') + self._arn_format = OU_ARN_FORMAT @property def arn(self): - return OU_ARN_FORMAT.format( + return self._arn_format.format( self.master_account_id, self.organization_id, self.id @@ -125,25 +129,18 @@ class FakeOrganizationalUnit(BaseModel): } -class FakeRoot(BaseModel): +class FakeRoot(FakeOrganizationalUnit): def __init__(self, organization, **kwargs): - self.organization_id = organization.id - self.master_account_id = organization.master_account_id - self.id = utils.make_random_root_id() + super().__init__(organization, **kwargs) + self.type = 'ROOT' + self.id = organization.root_id self.name = 'Root' self.policy_types = [{ 'Type': 'SERVICE_CONTROL_POLICY', 'Status': 'ENABLED' }] - - @property - def arn(self): - return ROOT_ARN_FORMAT.format( - self.master_account_id, - self.organization_id, - self.id - ) + self._arn_format = ROOT_ARN_FORMAT def describe(self): return { @@ -154,18 +151,16 @@ class FakeRoot(BaseModel): } - class OrganizationsBackend(BaseBackend): def __init__(self): self.org = None self.accounts = [] - self.roots = [] self.ou = [] def create_organization(self, **kwargs): self.org = FakeOrganization(kwargs['FeatureSet']) - self.roots.append(FakeRoot(self.org)) + self.ou.append(FakeRoot(self.org)) return self.org.describe() def describe_organization(self): @@ -173,11 +168,11 @@ class OrganizationsBackend(BaseBackend): def list_roots(self): return dict( - Roots=[root.describe() for root in self.roots] + Roots=[ou.describe() for ou in self.ou if isinstance(ou, FakeRoot)] ) def create_organizational_unit(self, **kwargs): - new_ou = FakeOrganizationalUnit(self.org, self.roots[0].id, **kwargs) + new_ou = FakeOrganizationalUnit(self.org, **kwargs) self.ou.append(new_ou) return new_ou.describe() @@ -201,23 +196,29 @@ class OrganizationsBackend(BaseBackend): ) def list_parents(self, **kwargs): - parent_id = [ - ou.parent_id for ou in self.ou if ou.id == kwargs['ChildId'] - ].pop(0) - root_parents = [ - dict(Id=root.id, Type='ROOT') - for root in self.roots - if root.id == parent_id - ] - ou_parents = [ - dict(Id=ou.id, Type='ORGANIZATIONAL_UNIT') - for ou in self.ou - if ou.id == parent_id - ] - return dict(Parents=root_parents + ou_parents) + if re.compile(r'[0-9]{12}').match(kwargs['ChildId']): + parent_id = [ + account.parent_id for account in self.accounts + if account.id == kwargs['ChildId'] + ].pop(0) + else: + parent_id = [ + ou.parent_id for ou in self.ou + if ou.id == kwargs['ChildId'] + ].pop(0) + return dict( + Parents=[ + { + 'Id': ou.id, + 'Type': ou.type, + } + for ou in self.ou + if ou.id == parent_id + ] + ) def create_account(self, **kwargs): - new_account = FakeAccount(self.org, self.roots[0].id, **kwargs) + new_account = FakeAccount(self.org, **kwargs) self.accounts.append(new_account) return new_account.create_account_status @@ -244,9 +245,10 @@ class OrganizationsBackend(BaseBackend): def move_account(self, **kwargs): new_parent_id = kwargs['DestinationParentId'] - all_parent_id = [parent.id for parent in self.roots + self.ou] + all_parent_id = [parent.id for parent in self.ou] account = [ - account for account in self.accounts if account.id == kwargs['AccountId'] + account for account in self.accounts + if account.id == kwargs['AccountId'] ].pop(0) assert new_parent_id in all_parent_id assert account.parent_id == kwargs['SourceParentId'] diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 5355e2716..2b692489f 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -198,30 +198,6 @@ def test_list_organizational_units_for_parent(): #assert False -@mock_organizations -def test_list_parents(): - client = boto3.client('organizations', region_name='us-east-1') - org = client.create_organization(FeatureSet='ALL')['Organization'] - root_id = client.list_roots()['Roots'][0]['Id'] - - ou01 = client.create_organizational_unit(ParentId=root_id, Name='ou01') - ou01_id = ou01['OrganizationalUnit']['Id'] - response01 = client.list_parents(ChildId=ou01_id) - #print(yaml.dump(response01, default_flow_style=False)) - response01.should.have.key('Parents').should.be.a(list) - response01['Parents'][0].should.have.key('Id').should.equal(root_id) - response01['Parents'][0].should.have.key('Type').should.equal('ROOT') - - ou02 = client.create_organizational_unit(ParentId=ou01_id, Name='ou02') - ou02_id = ou02['OrganizationalUnit']['Id'] - response02 = client.list_parents(ChildId=ou02_id) - #print(yaml.dump(response02, default_flow_style=False)) - response02.should.have.key('Parents').should.be.a(list) - response02['Parents'][0].should.have.key('Id').should.equal(ou01_id) - response02['Parents'][0].should.have.key('Type').should.equal('ORGANIZATIONAL_UNIT') - #assert False - - # Accounts mockname = 'mock-account' mockdomain = 'moto-example.org' @@ -310,3 +286,58 @@ def test_move_account(): #print(yaml.dump(response, default_flow_style=False)) account_id.should.be.within([account['Id'] for account in response['Accounts']]) #assert False + + +@mock_organizations +def test_list_parents_for_ou(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + ou01 = client.create_organizational_unit(ParentId=root_id, Name='ou01') + ou01_id = ou01['OrganizationalUnit']['Id'] + response01 = client.list_parents(ChildId=ou01_id) + #print(yaml.dump(response01, default_flow_style=False)) + response01.should.have.key('Parents').should.be.a(list) + response01['Parents'][0].should.have.key('Id').should.equal(root_id) + response01['Parents'][0].should.have.key('Type').should.equal('ROOT') + ou02 = client.create_organizational_unit(ParentId=ou01_id, Name='ou02') + ou02_id = ou02['OrganizationalUnit']['Id'] + response02 = client.list_parents(ChildId=ou02_id) + #print(yaml.dump(response02, default_flow_style=False)) + response02.should.have.key('Parents').should.be.a(list) + response02['Parents'][0].should.have.key('Id').should.equal(ou01_id) + response02['Parents'][0].should.have.key('Type').should.equal('ORGANIZATIONAL_UNIT') + #assert False + + +@mock_organizations +def test_list_parents_for_accounts(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + ou01 = client.create_organizational_unit(ParentId=root_id, Name='ou01') + ou01_id = ou01['OrganizationalUnit']['Id'] + account01_id = client.create_account( + AccountName='account01', + Email='account01@moto-example.org' + )['CreateAccountStatus']['AccountId'] + account02_id = client.create_account( + AccountName='account02', + Email='account02@moto-example.org' + )['CreateAccountStatus']['AccountId'] + client.move_account( + AccountId=account02_id, + SourceParentId=root_id, + DestinationParentId=ou01_id, + ) + response01 = client.list_parents(ChildId=account01_id) + #print(yaml.dump(response01, default_flow_style=False)) + response01.should.have.key('Parents').should.be.a(list) + response01['Parents'][0].should.have.key('Id').should.equal(root_id) + response01['Parents'][0].should.have.key('Type').should.equal('ROOT') + response02 = client.list_parents(ChildId=account02_id) + #print(yaml.dump(response02, default_flow_style=False)) + response02.should.have.key('Parents').should.be.a(list) + response02['Parents'][0].should.have.key('Id').should.equal(ou01_id) + response02['Parents'][0].should.have.key('Type').should.equal('ORGANIZATIONAL_UNIT') + #assert False From 8f400b7110b7804ab7445c5aab3e909376833e79 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Sun, 15 Jul 2018 22:19:42 -0700 Subject: [PATCH 10/16] organizations: add endpoint list_chilren --- moto/organizations/models.py | 60 ++++++++++++------- moto/organizations/responses.py | 5 ++ .../test_organizations_boto3.py | 41 +++++++++++++ 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 609ce3831..2e0d6b40d 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -195,28 +195,6 @@ class OrganizationsBackend(BaseBackend): ] ) - def list_parents(self, **kwargs): - if re.compile(r'[0-9]{12}').match(kwargs['ChildId']): - parent_id = [ - account.parent_id for account in self.accounts - if account.id == kwargs['ChildId'] - ].pop(0) - else: - parent_id = [ - ou.parent_id for ou in self.ou - if ou.id == kwargs['ChildId'] - ].pop(0) - return dict( - Parents=[ - { - 'Id': ou.id, - 'Type': ou.type, - } - for ou in self.ou - if ou.id == parent_id - ] - ) - def create_account(self, **kwargs): new_account = FakeAccount(self.org, **kwargs) self.accounts.append(new_account) @@ -255,5 +233,43 @@ class OrganizationsBackend(BaseBackend): index = self.accounts.index(account) self.accounts[index].parent_id = new_parent_id + def list_parents(self, **kwargs): + if re.compile(r'[0-9]{12}').match(kwargs['ChildId']): + obj_list = self.accounts + else: + obj_list = self.ou + parent_id = [ + obj.parent_id for obj in obj_list + if obj.id == kwargs['ChildId'] + ].pop(0) + return dict( + Parents=[ + { + 'Id': ou.id, + 'Type': ou.type, + } + for ou in self.ou + if ou.id == parent_id + ] + ) + + def list_children(self, **kwargs): + if kwargs['ChildType'] == 'ACCOUNT': + obj_list = self.accounts + elif kwargs['ChildType'] == 'ORGANIZATIONAL_UNIT': + obj_list = self.ou + else: + raise ValueError + return dict( + Children=[ + { + 'Id': obj.id, + 'Type': kwargs['ChildType'], + } + for obj in obj_list + if obj.parent_id == kwargs['ParentId'] + ] + ) + organizations_backend = OrganizationsBackend() diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 6684ae685..966c3fbf3 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -80,3 +80,8 @@ class OrganizationsResponse(BaseResponse): return json.dumps( self.organizations_backend.move_account(**self.request_params) ) + + def list_children(self): + return json.dumps( + self.organizations_backend.list_children(**self.request_params) + ) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 2b692489f..496bcd74d 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -341,3 +341,44 @@ def test_list_parents_for_accounts(): response02['Parents'][0].should.have.key('Id').should.equal(ou01_id) response02['Parents'][0].should.have.key('Type').should.equal('ORGANIZATIONAL_UNIT') #assert False + + +@mock_organizations +def test_list_chidlren(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + ou01 = client.create_organizational_unit(ParentId=root_id, Name='ou01') + ou01_id = ou01['OrganizationalUnit']['Id'] + ou02 = client.create_organizational_unit(ParentId=ou01_id, Name='ou02') + ou02_id = ou02['OrganizationalUnit']['Id'] + account01_id = client.create_account( + AccountName='account01', + Email='account01@moto-example.org' + )['CreateAccountStatus']['AccountId'] + account02_id = client.create_account( + AccountName='account02', + Email='account02@moto-example.org' + )['CreateAccountStatus']['AccountId'] + client.move_account( + AccountId=account02_id, + SourceParentId=root_id, + DestinationParentId=ou01_id, + ) + response01 = client.list_children(ParentId=root_id, ChildType='ACCOUNT') + response02 = client.list_children(ParentId=root_id, ChildType='ORGANIZATIONAL_UNIT') + response03 = client.list_children(ParentId=ou01_id, ChildType='ACCOUNT') + response04 = client.list_children(ParentId=ou01_id, ChildType='ORGANIZATIONAL_UNIT') + #print(yaml.dump(response01, default_flow_style=False)) + #print(yaml.dump(response02, default_flow_style=False)) + #print(yaml.dump(response03, default_flow_style=False)) + #print(yaml.dump(response04, default_flow_style=False)) + response01['Children'][0]['Id'].should.equal(account01_id) + response01['Children'][0]['Type'].should.equal('ACCOUNT') + response02['Children'][0]['Id'].should.equal(ou01_id) + response02['Children'][0]['Type'].should.equal('ORGANIZATIONAL_UNIT') + response03['Children'][0]['Id'].should.equal(account02_id) + response03['Children'][0]['Type'].should.equal('ACCOUNT') + response04['Children'][0]['Id'].should.equal(ou02_id) + response04['Children'][0]['Type'].should.equal('ORGANIZATIONAL_UNIT') + #assert False From 01912bdca7e2603035c4905c0274f835045d2a2a Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Mon, 16 Jul 2018 07:23:06 -0700 Subject: [PATCH 11/16] organizations: fix python 2.7 test errors --- moto/organizations/models.py | 2 +- tests/test_organizations/test_organizations_boto3.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 2e0d6b40d..a165395f7 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -132,7 +132,7 @@ class FakeOrganizationalUnit(BaseModel): class FakeRoot(FakeOrganizationalUnit): def __init__(self, organization, **kwargs): - super().__init__(organization, **kwargs) + super(FakeRoot, self).__init__(organization, **kwargs) self.type = 'ROOT' self.id = organization.root_id self.name = 'Root' diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 496bcd74d..0ac2b61bf 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -4,6 +4,7 @@ import boto3 import sure # noqa import datetime import yaml +import six from moto import mock_organizations from moto.organizations.models import ( @@ -64,7 +65,7 @@ def validate_organizational_unit(org, response): org['Id'], ou['Id'], )) - ou.should.have.key('Name').should.be.a(str) + ou.should.have.key('Name').should.be.a(six.string_types) def validate_account(org, account): @@ -86,7 +87,7 @@ def validate_account(org, account): account['Email'].should.match(EMAIL_REGEX) account['JoinedMethod'].should.be.within(['INVITED', 'CREATED']) account['Status'].should.be.within(['ACTIVE', 'SUSPENDED']) - account['Name'].should.be.a(str) + account['Name'].should.be.a(six.string_types) account['JoinedTimestamp'].should.be.a(datetime.datetime) @@ -101,7 +102,7 @@ def validate_create_account_status(create_status): ]) create_status['Id'].should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) create_status['AccountId'].should.match(ACCOUNT_ID_REGEX) - create_status['AccountName'].should.be.a(str) + create_status['AccountName'].should.be.a(six.string_types) create_status['State'].should.equal('SUCCEEDED') create_status['RequestedTimestamp'].should.be.a(datetime.datetime) create_status['CompletedTimestamp'].should.be.a(datetime.datetime) @@ -144,7 +145,7 @@ def test_list_roots(): org['Id'], root['Id'], )) - root.should.have.key('Name').should.be.a(str) + root.should.have.key('Name').should.be.a(six.string_types) root.should.have.key('PolicyTypes').should.be.a(list) root['PolicyTypes'][0].should.have.key('Type').should.equal('SERVICE_CONTROL_POLICY') root['PolicyTypes'][0].should.have.key('Status').should.equal('ENABLED') From 40e422b74d56e545f4a1a903213a56bebff5c53f Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Thu, 19 Jul 2018 11:50:24 -0700 Subject: [PATCH 12/16] [issue #1720] Add support for AWS Organizations ready for pull request did a little cleanup refactoring local tests pass --- moto/organizations/models.py | 22 +-- moto/organizations/utils.py | 8 + tests/test_organizations/object_syntax.py | 25 --- .../organizations_test_utils.py | 136 ++++++++++++++++ .../test_organizations_boto3.py | 153 +----------------- .../test_organizations_utils.py | 36 ----- 6 files changed, 158 insertions(+), 222 deletions(-) delete mode 100644 tests/test_organizations/object_syntax.py create mode 100644 tests/test_organizations/organizations_test_utils.py delete mode 100644 tests/test_organizations/test_organizations_utils.py diff --git a/moto/organizations/models.py b/moto/organizations/models.py index a165395f7..0f3c67400 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -7,14 +7,6 @@ from moto.core import BaseBackend, BaseModel from moto.core.utils import unix_time from moto.organizations import utils -MASTER_ACCOUNT_ID = '123456789012' -MASTER_ACCOUNT_EMAIL = 'fakeorg@moto-example.com' -ORGANIZATION_ARN_FORMAT = 'arn:aws:organizations::{0}:organization/{1}' -MASTER_ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{0}' -ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{2}' -ROOT_ARN_FORMAT = 'arn:aws:organizations::{0}:root/{1}/{2}' -OU_ARN_FORMAT = 'arn:aws:organizations::{0}:ou/{1}/{2}' - class FakeOrganization(BaseModel): @@ -22,8 +14,8 @@ class FakeOrganization(BaseModel): self.id = utils.make_random_org_id() self.root_id = utils.make_random_root_id() self.feature_set = feature_set - self.master_account_id = MASTER_ACCOUNT_ID - self.master_account_email = MASTER_ACCOUNT_EMAIL + self.master_account_id = utils.MASTER_ACCOUNT_ID + self.master_account_email = utils.MASTER_ACCOUNT_EMAIL self.available_policy_types = [{ 'Type': 'SERVICE_CONTROL_POLICY', 'Status': 'ENABLED' @@ -31,11 +23,11 @@ class FakeOrganization(BaseModel): @property def arn(self): - return ORGANIZATION_ARN_FORMAT.format(self.master_account_id, self.id) + return utils.ORGANIZATION_ARN_FORMAT.format(self.master_account_id, self.id) @property def master_account_arn(self): - return MASTER_ACCOUNT_ARN_FORMAT.format(self.master_account_id, self.id) + return utils.MASTER_ACCOUNT_ARN_FORMAT.format(self.master_account_id, self.id) def describe(self): return { @@ -67,7 +59,7 @@ class FakeAccount(BaseModel): @property def arn(self): - return ACCOUNT_ARN_FORMAT.format( + return utils.ACCOUNT_ARN_FORMAT.format( self.master_account_id, self.organization_id, self.id @@ -109,7 +101,7 @@ class FakeOrganizationalUnit(BaseModel): self.id = utils.make_random_ou_id(organization.root_id) self.name = kwargs.get('Name') self.parent_id = kwargs.get('ParentId') - self._arn_format = OU_ARN_FORMAT + self._arn_format = utils.OU_ARN_FORMAT @property def arn(self): @@ -140,7 +132,7 @@ class FakeRoot(FakeOrganizationalUnit): 'Type': 'SERVICE_CONTROL_POLICY', 'Status': 'ENABLED' }] - self._arn_format = ROOT_ARN_FORMAT + self._arn_format = utils.ROOT_ARN_FORMAT def describe(self): return { diff --git a/moto/organizations/utils.py b/moto/organizations/utils.py index c7e5c71cd..007afa6ed 100644 --- a/moto/organizations/utils.py +++ b/moto/organizations/utils.py @@ -3,6 +3,14 @@ from __future__ import unicode_literals import random import string +MASTER_ACCOUNT_ID = '123456789012' +MASTER_ACCOUNT_EMAIL = 'fakeorg@moto-example.com' +ORGANIZATION_ARN_FORMAT = 'arn:aws:organizations::{0}:organization/{1}' +MASTER_ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{0}' +ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{2}' +ROOT_ARN_FORMAT = 'arn:aws:organizations::{0}:root/{1}/{2}' +OU_ARN_FORMAT = 'arn:aws:organizations::{0}:ou/{1}/{2}' + CHARSET = string.ascii_lowercase + string.digits ORG_ID_SIZE = 10 ROOT_ID_SIZE = 4 diff --git a/tests/test_organizations/object_syntax.py b/tests/test_organizations/object_syntax.py deleted file mode 100644 index 3fb86b9d5..000000000 --- a/tests/test_organizations/object_syntax.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Temporary functions for checking object structures while specing out -models. This module will go away. -""" - -import yaml -from moto import organizations as orgs - - -# utils -print(orgs.utils.make_random_org_id()) -root_id = orgs.utils.make_random_root_id() -print(root_id) -print(orgs.utils.make_random_ou_id(root_id)) -print(orgs.utils.make_random_account_id()) -print(orgs.utils.make_random_create_account_status_id()) - -# models -my_org = orgs.models.FakeOrganization(feature_set='ALL') -print(yaml.dump(my_org._describe())) -#assert False - -my_account = orgs.models.FakeAccount(my_org, AccountName='blee01', Email='blee01@moto-example.org') -print(yaml.dump(my_account)) -#assert False diff --git a/tests/test_organizations/organizations_test_utils.py b/tests/test_organizations/organizations_test_utils.py new file mode 100644 index 000000000..6548b1830 --- /dev/null +++ b/tests/test_organizations/organizations_test_utils.py @@ -0,0 +1,136 @@ +from __future__ import unicode_literals + +import six +import sure # noqa +import datetime +from moto.organizations import utils + +EMAIL_REGEX = "^.+@[a-zA-Z0-9-.]+.[a-zA-Z]{2,3}|[0-9]{1,3}$" +ORG_ID_REGEX = r'o-[a-z0-9]{%s}' % utils.ORG_ID_SIZE +ROOT_ID_REGEX = r'r-[a-z0-9]{%s}' % utils.ROOT_ID_SIZE +OU_ID_REGEX = r'ou-[a-z0-9]{%s}-[a-z0-9]{%s}' % (utils.ROOT_ID_SIZE, utils.OU_ID_SUFFIX_SIZE) +ACCOUNT_ID_REGEX = r'[0-9]{%s}' % utils.ACCOUNT_ID_SIZE +CREATE_ACCOUNT_STATUS_ID_REGEX = r'car-[a-z0-9]{%s}' % utils.CREATE_ACCOUNT_STATUS_ID_SIZE + + +def test_make_random_org_id(): + org_id = utils.make_random_org_id() + org_id.should.match(ORG_ID_REGEX) + + +def test_make_random_root_id(): + root_id = utils.make_random_root_id() + root_id.should.match(ROOT_ID_REGEX) + + +def test_make_random_ou_id(): + root_id = utils.make_random_root_id() + ou_id = utils.make_random_ou_id(root_id) + ou_id.should.match(OU_ID_REGEX) + + +def test_make_random_account_id(): + account_id = utils.make_random_account_id() + account_id.should.match(ACCOUNT_ID_REGEX) + + +def test_make_random_create_account_status_id(): + create_account_status_id = utils.make_random_create_account_status_id() + create_account_status_id.should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) + + +def validate_organization(response): + org = response['Organization'] + sorted(org.keys()).should.equal([ + 'Arn', + 'AvailablePolicyTypes', + 'FeatureSet', + 'Id', + 'MasterAccountArn', + 'MasterAccountEmail', + 'MasterAccountId', + ]) + org['Id'].should.match(ORG_ID_REGEX) + org['MasterAccountId'].should.equal(utils.MASTER_ACCOUNT_ID) + org['MasterAccountArn'].should.equal(utils.MASTER_ACCOUNT_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + )) + org['Arn'].should.equal(utils.ORGANIZATION_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + )) + org['MasterAccountEmail'].should.equal(utils.MASTER_ACCOUNT_EMAIL) + org['FeatureSet'].should.be.within(['ALL', 'CONSOLIDATED_BILLING']) + org['AvailablePolicyTypes'].should.equal([{ + 'Type': 'SERVICE_CONTROL_POLICY', + 'Status': 'ENABLED' + }]) + + +def validate_roots(org, response): + response.should.have.key('Roots').should.be.a(list) + response['Roots'].should_not.be.empty + root = response['Roots'][0] + root.should.have.key('Id').should.match(ROOT_ID_REGEX) + root.should.have.key('Arn').should.equal(utils.ROOT_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + root['Id'], + )) + root.should.have.key('Name').should.be.a(six.string_types) + root.should.have.key('PolicyTypes').should.be.a(list) + root['PolicyTypes'][0].should.have.key('Type').should.equal('SERVICE_CONTROL_POLICY') + root['PolicyTypes'][0].should.have.key('Status').should.equal('ENABLED') + + +def validate_organizational_unit(org, response): + response.should.have.key('OrganizationalUnit').should.be.a(dict) + ou = response['OrganizationalUnit'] + ou.should.have.key('Id').should.match(OU_ID_REGEX) + ou.should.have.key('Arn').should.equal(utils.OU_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + ou['Id'], + )) + ou.should.have.key('Name').should.be.a(six.string_types) + + +def validate_account(org, account): + sorted(account.keys()).should.equal([ + 'Arn', + 'Email', + 'Id', + 'JoinedMethod', + 'JoinedTimestamp', + 'Name', + 'Status', + ]) + account['Id'].should.match(ACCOUNT_ID_REGEX) + account['Arn'].should.equal(utils.ACCOUNT_ARN_FORMAT.format( + org['MasterAccountId'], + org['Id'], + account['Id'], + )) + account['Email'].should.match(EMAIL_REGEX) + account['JoinedMethod'].should.be.within(['INVITED', 'CREATED']) + account['Status'].should.be.within(['ACTIVE', 'SUSPENDED']) + account['Name'].should.be.a(six.string_types) + account['JoinedTimestamp'].should.be.a(datetime.datetime) + + +def validate_create_account_status(create_status): + sorted(create_status.keys()).should.equal([ + 'AccountId', + 'AccountName', + 'CompletedTimestamp', + 'Id', + 'RequestedTimestamp', + 'State', + ]) + create_status['Id'].should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) + create_status['AccountId'].should.match(ACCOUNT_ID_REGEX) + create_status['AccountName'].should.be.a(six.string_types) + create_status['State'].should.equal('SUCCEEDED') + create_status['RequestedTimestamp'].should.be.a(datetime.datetime) + create_status['CompletedTimestamp'].should.be.a(datetime.datetime) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index 0ac2b61bf..c2f06774a 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -2,120 +2,24 @@ from __future__ import unicode_literals import boto3 import sure # noqa -import datetime import yaml -import six from moto import mock_organizations -from moto.organizations.models import ( - MASTER_ACCOUNT_ID, - MASTER_ACCOUNT_EMAIL, - ORGANIZATION_ARN_FORMAT, - MASTER_ACCOUNT_ARN_FORMAT, - ACCOUNT_ARN_FORMAT, - ROOT_ARN_FORMAT, - OU_ARN_FORMAT, +from .organizations_test_utils import ( + validate_organization, + validate_roots, + validate_organizational_unit, + validate_account, + validate_create_account_status, ) -from .test_organizations_utils import ( - ORG_ID_REGEX, - ROOT_ID_REGEX, - OU_ID_REGEX, - ACCOUNT_ID_REGEX, - CREATE_ACCOUNT_STATUS_ID_REGEX, -) - -EMAIL_REGEX = "^.+@[a-zA-Z0-9-.]+.[a-zA-Z]{2,3}|[0-9]{1,3}$" - - -def validate_organization(response): - org = response['Organization'] - sorted(org.keys()).should.equal([ - 'Arn', - 'AvailablePolicyTypes', - 'FeatureSet', - 'Id', - 'MasterAccountArn', - 'MasterAccountEmail', - 'MasterAccountId', - ]) - org['Id'].should.match(ORG_ID_REGEX) - org['MasterAccountId'].should.equal(MASTER_ACCOUNT_ID) - org['MasterAccountArn'].should.equal(MASTER_ACCOUNT_ARN_FORMAT.format( - org['MasterAccountId'], - org['Id'], - )) - org['Arn'].should.equal(ORGANIZATION_ARN_FORMAT.format( - org['MasterAccountId'], - org['Id'], - )) - org['MasterAccountEmail'].should.equal(MASTER_ACCOUNT_EMAIL) - org['FeatureSet'].should.be.within(['ALL', 'CONSOLIDATED_BILLING']) - org['AvailablePolicyTypes'].should.equal([{ - 'Type': 'SERVICE_CONTROL_POLICY', - 'Status': 'ENABLED' - }]) - - -def validate_organizational_unit(org, response): - response.should.have.key('OrganizationalUnit').should.be.a(dict) - ou = response['OrganizationalUnit'] - ou.should.have.key('Id').should.match(OU_ID_REGEX) - ou.should.have.key('Arn').should.equal(OU_ARN_FORMAT.format( - org['MasterAccountId'], - org['Id'], - ou['Id'], - )) - ou.should.have.key('Name').should.be.a(six.string_types) - - -def validate_account(org, account): - sorted(account.keys()).should.equal([ - 'Arn', - 'Email', - 'Id', - 'JoinedMethod', - 'JoinedTimestamp', - 'Name', - 'Status', - ]) - account['Id'].should.match(ACCOUNT_ID_REGEX) - account['Arn'].should.equal(ACCOUNT_ARN_FORMAT.format( - org['MasterAccountId'], - org['Id'], - account['Id'], - )) - account['Email'].should.match(EMAIL_REGEX) - account['JoinedMethod'].should.be.within(['INVITED', 'CREATED']) - account['Status'].should.be.within(['ACTIVE', 'SUSPENDED']) - account['Name'].should.be.a(six.string_types) - account['JoinedTimestamp'].should.be.a(datetime.datetime) - - -def validate_create_account_status(create_status): - sorted(create_status.keys()).should.equal([ - 'AccountId', - 'AccountName', - 'CompletedTimestamp', - 'Id', - 'RequestedTimestamp', - 'State', - ]) - create_status['Id'].should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) - create_status['AccountId'].should.match(ACCOUNT_ID_REGEX) - create_status['AccountName'].should.be.a(six.string_types) - create_status['State'].should.equal('SUCCEEDED') - create_status['RequestedTimestamp'].should.be.a(datetime.datetime) - create_status['CompletedTimestamp'].should.be.a(datetime.datetime) @mock_organizations def test_create_organization(): client = boto3.client('organizations', region_name='us-east-1') response = client.create_organization(FeatureSet='ALL') - #print(yaml.dump(response)) validate_organization(response) response['Organization']['FeatureSet'].should.equal('ALL') - #assert False @mock_organizations @@ -123,9 +27,7 @@ def test_describe_organization(): client = boto3.client('organizations', region_name='us-east-1') client.create_organization(FeatureSet='ALL') response = client.describe_organization() - #print(yaml.dump(response)) validate_organization(response) - #assert False # Organizational Units @@ -135,21 +37,7 @@ def test_list_roots(): client = boto3.client('organizations', region_name='us-east-1') org = client.create_organization(FeatureSet='ALL')['Organization'] response = client.list_roots() - #print(yaml.dump(response, default_flow_style=False)) - response.should.have.key('Roots').should.be.a(list) - response['Roots'].should_not.be.empty - root = response['Roots'][0] - root.should.have.key('Id').should.match(ROOT_ID_REGEX) - root.should.have.key('Arn').should.equal(ROOT_ARN_FORMAT.format( - org['MasterAccountId'], - org['Id'], - root['Id'], - )) - root.should.have.key('Name').should.be.a(six.string_types) - root.should.have.key('PolicyTypes').should.be.a(list) - root['PolicyTypes'][0].should.have.key('Type').should.equal('SERVICE_CONTROL_POLICY') - root['PolicyTypes'][0].should.have.key('Status').should.equal('ENABLED') - #assert False + validate_roots(org, response) @mock_organizations @@ -162,10 +50,8 @@ def test_create_organizational_unit(): ParentId=root_id, Name=ou_name, ) - #print(yaml.dump(response, default_flow_style=False)) validate_organizational_unit(org, response) response['OrganizationalUnit']['Name'].should.equal(ou_name) - #assert False @mock_organizations @@ -178,9 +64,7 @@ def test_describe_organizational_unit(): Name='ou01', )['OrganizationalUnit']['Id'] response = client.describe_organizational_unit(OrganizationalUnitId=ou_id) - print(yaml.dump(response, default_flow_style=False)) validate_organizational_unit(org, response) - #assert False @mock_organizations @@ -192,11 +76,9 @@ def test_list_organizational_units_for_parent(): client.create_organizational_unit(ParentId=root_id, Name='ou02') client.create_organizational_unit(ParentId=root_id, Name='ou03') response = client.list_organizational_units_for_parent(ParentId=root_id) - print(yaml.dump(response, default_flow_style=False)) response.should.have.key('OrganizationalUnits').should.be.a(list) for ou in response['OrganizationalUnits']: validate_organizational_unit(org, dict(OrganizationalUnit=ou)) - #assert False # Accounts @@ -212,10 +94,8 @@ def test_create_account(): create_status = client.create_account( AccountName=mockname, Email=mockemail )['CreateAccountStatus'] - #print(yaml.dump(create_status, default_flow_style=False)) validate_create_account_status(create_status) create_status['AccountName'].should.equal(mockname) - #assert False @mock_organizations @@ -226,11 +106,9 @@ def test_describe_account(): AccountName=mockname, Email=mockemail )['CreateAccountStatus']['AccountId'] response = client.describe_account(AccountId=account_id) - #print(yaml.dump(response, default_flow_style=False)) validate_account(org, response['Account']) response['Account']['Name'].should.equal(mockname) response['Account']['Email'].should.equal(mockemail) - #assert False @mock_organizations @@ -242,7 +120,6 @@ def test_list_accounts(): email = name + '@' + mockdomain client.create_account(AccountName=name, Email=email) response = client.list_accounts() - #print(yaml.dump(response, default_flow_style=False)) response.should.have.key('Accounts') accounts = response['Accounts'] len(accounts).should.equal(5) @@ -250,7 +127,6 @@ def test_list_accounts(): validate_account(org, account) accounts[3]['Name'].should.equal(mockname + '3') accounts[2]['Email'].should.equal(mockname + '2' + '@' + mockdomain) - #assert False @mock_organizations @@ -263,9 +139,7 @@ def test_list_accounts_for_parent(): Email=mockemail, )['CreateAccountStatus']['AccountId'] response = client.list_accounts_for_parent(ParentId=root_id) - #print(yaml.dump(response, default_flow_style=False)) account_id.should.be.within([account['Id'] for account in response['Accounts']]) - #assert False @mock_organizations @@ -284,9 +158,7 @@ def test_move_account(): DestinationParentId=ou01_id, ) response = client.list_accounts_for_parent(ParentId=ou01_id) - #print(yaml.dump(response, default_flow_style=False)) account_id.should.be.within([account['Id'] for account in response['Accounts']]) - #assert False @mock_organizations @@ -297,18 +169,15 @@ def test_list_parents_for_ou(): ou01 = client.create_organizational_unit(ParentId=root_id, Name='ou01') ou01_id = ou01['OrganizationalUnit']['Id'] response01 = client.list_parents(ChildId=ou01_id) - #print(yaml.dump(response01, default_flow_style=False)) response01.should.have.key('Parents').should.be.a(list) response01['Parents'][0].should.have.key('Id').should.equal(root_id) response01['Parents'][0].should.have.key('Type').should.equal('ROOT') ou02 = client.create_organizational_unit(ParentId=ou01_id, Name='ou02') ou02_id = ou02['OrganizationalUnit']['Id'] response02 = client.list_parents(ChildId=ou02_id) - #print(yaml.dump(response02, default_flow_style=False)) response02.should.have.key('Parents').should.be.a(list) response02['Parents'][0].should.have.key('Id').should.equal(ou01_id) response02['Parents'][0].should.have.key('Type').should.equal('ORGANIZATIONAL_UNIT') - #assert False @mock_organizations @@ -332,16 +201,13 @@ def test_list_parents_for_accounts(): DestinationParentId=ou01_id, ) response01 = client.list_parents(ChildId=account01_id) - #print(yaml.dump(response01, default_flow_style=False)) response01.should.have.key('Parents').should.be.a(list) response01['Parents'][0].should.have.key('Id').should.equal(root_id) response01['Parents'][0].should.have.key('Type').should.equal('ROOT') response02 = client.list_parents(ChildId=account02_id) - #print(yaml.dump(response02, default_flow_style=False)) response02.should.have.key('Parents').should.be.a(list) response02['Parents'][0].should.have.key('Id').should.equal(ou01_id) response02['Parents'][0].should.have.key('Type').should.equal('ORGANIZATIONAL_UNIT') - #assert False @mock_organizations @@ -370,10 +236,6 @@ def test_list_chidlren(): response02 = client.list_children(ParentId=root_id, ChildType='ORGANIZATIONAL_UNIT') response03 = client.list_children(ParentId=ou01_id, ChildType='ACCOUNT') response04 = client.list_children(ParentId=ou01_id, ChildType='ORGANIZATIONAL_UNIT') - #print(yaml.dump(response01, default_flow_style=False)) - #print(yaml.dump(response02, default_flow_style=False)) - #print(yaml.dump(response03, default_flow_style=False)) - #print(yaml.dump(response04, default_flow_style=False)) response01['Children'][0]['Id'].should.equal(account01_id) response01['Children'][0]['Type'].should.equal('ACCOUNT') response02['Children'][0]['Id'].should.equal(ou01_id) @@ -382,4 +244,3 @@ def test_list_chidlren(): response03['Children'][0]['Type'].should.equal('ACCOUNT') response04['Children'][0]['Id'].should.equal(ou02_id) response04['Children'][0]['Type'].should.equal('ORGANIZATIONAL_UNIT') - #assert False diff --git a/tests/test_organizations/test_organizations_utils.py b/tests/test_organizations/test_organizations_utils.py deleted file mode 100644 index d27201446..000000000 --- a/tests/test_organizations/test_organizations_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import unicode_literals - -import sure # noqa -from moto.organizations import utils - -ORG_ID_REGEX = r'o-[a-z0-9]{%s}' % utils.ORG_ID_SIZE -ROOT_ID_REGEX = r'r-[a-z0-9]{%s}' % utils.ROOT_ID_SIZE -OU_ID_REGEX = r'ou-[a-z0-9]{%s}-[a-z0-9]{%s}' % (utils.ROOT_ID_SIZE, utils.OU_ID_SUFFIX_SIZE) -ACCOUNT_ID_REGEX = r'[0-9]{%s}' % utils.ACCOUNT_ID_SIZE -CREATE_ACCOUNT_STATUS_ID_REGEX = r'car-[a-z0-9]{%s}' % utils.CREATE_ACCOUNT_STATUS_ID_SIZE - - -def test_make_random_org_id(): - org_id = utils.make_random_org_id() - org_id.should.match(ORG_ID_REGEX) - - -def test_make_random_root_id(): - root_id = utils.make_random_root_id() - root_id.should.match(ROOT_ID_REGEX) - - -def test_make_random_ou_id(): - root_id = utils.make_random_root_id() - ou_id = utils.make_random_ou_id(root_id) - ou_id.should.match(OU_ID_REGEX) - - -def test_make_random_account_id(): - account_id = utils.make_random_account_id() - account_id.should.match(ACCOUNT_ID_REGEX) - - -def test_make_random_create_account_status_id(): - create_account_status_id = utils.make_random_create_account_status_id() - create_account_status_id.should.match(CREATE_ACCOUNT_STATUS_ID_REGEX) From 05928b1497831e38a642baadc438b8238d65b4bf Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Fri, 20 Jul 2018 13:54:53 -0700 Subject: [PATCH 13/16] [issue #1720] Add support for AWS Organizations added exception handling in class OrganizationsBackend --- IMPLEMENTATION_COVERAGE.md | 4 +- README.md | 2 +- moto/organizations/models.py | 76 ++++++++++++------- .../test_organizations_boto3.py | 69 ++++++++++++++++- 4 files changed, 120 insertions(+), 31 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index e1943aa9a..d19d2473e 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3147,7 +3147,7 @@ - [ ] update_server - [ ] update_server_engine_attributes -## organizations - 28% implemented +## organizations - 30% implemented - [ ] accept_handshake - [ ] attach_policy - [ ] cancel_handshake @@ -3176,7 +3176,7 @@ - [X] list_accounts - [X] list_accounts_for_parent - [ ] list_aws_service_access_for_organization -- [ ] list_children +- [X] list_children - [ ] list_create_account_status - [ ] list_handshakes_for_account - [ ] list_handshakes_for_organization diff --git a/README.md b/README.md index e4fcb6508..189bf2c4f 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | KMS | @mock_kms | basic endpoints done | |------------------------------------------------------------------------------| -| Organizations | @mock_organizations | some endpoints done | +| Organizations | @mock_organizations | some core endpoints done | |------------------------------------------------------------------------------| | Polly | @mock_polly | all endpoints done | |------------------------------------------------------------------------------| diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 0f3c67400..91896f537 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -4,6 +4,7 @@ import datetime import re from moto.core import BaseBackend, BaseModel +from moto.core.exceptions import RESTError from moto.core.utils import unix_time from moto.organizations import utils @@ -168,13 +169,31 @@ class OrganizationsBackend(BaseBackend): self.ou.append(new_ou) return new_ou.describe() + def get_organizational_unit_by_id(self, ou_id): + ou = next((ou for ou in self.ou if ou.id == ou_id), None) + if ou is None: + raise RESTError( + 'OrganizationalUnitNotFoundException', + "You specified an organizational unit that doesn't exist." + ) + return ou + + def validate_parent_id(self, parent_id): + try: + self.get_organizational_unit_by_id(parent_id) + except RESTError as e: + raise RESTError( + 'ParentNotFoundException', + "You specified parent that doesn't exist." + ) + return parent_id + def describe_organizational_unit(self, **kwargs): - ou = [ - ou for ou in self.ou if ou.id == kwargs['OrganizationalUnitId'] - ].pop(0) + ou = self.get_organizational_unit_by_id(kwargs['OrganizationalUnitId']) return ou.describe() def list_organizational_units_for_parent(self, **kwargs): + parent_id = self.validate_parent_id(kwargs['ParentId']) return dict( OrganizationalUnits=[ { @@ -183,7 +202,7 @@ class OrganizationsBackend(BaseBackend): 'Name': ou.name, } for ou in self.ou - if ou.parent_id == kwargs['ParentId'] + if ou.parent_id == parent_id ] ) @@ -192,11 +211,20 @@ class OrganizationsBackend(BaseBackend): self.accounts.append(new_account) return new_account.create_account_status - def describe_account(self, **kwargs): - account = [ + def get_account_by_id(self, account_id): + account = next(( account for account in self.accounts - if account.id == kwargs['AccountId'] - ].pop(0) + if account.id == account_id + ), None) + if account is None: + raise RESTError( + 'AccountNotFoundException', + "You specified an account that doesn't exist." + ) + return account + + def describe_account(self, **kwargs): + account = self.get_account_by_id(kwargs['AccountId']) return account.describe() def list_accounts(self): @@ -205,35 +233,27 @@ class OrganizationsBackend(BaseBackend): ) def list_accounts_for_parent(self, **kwargs): + parent_id = self.validate_parent_id(kwargs['ParentId']) return dict( Accounts=[ account.describe()['Account'] for account in self.accounts - if account.parent_id == kwargs['ParentId'] + if account.parent_id == parent_id ] ) def move_account(self, **kwargs): - new_parent_id = kwargs['DestinationParentId'] - all_parent_id = [parent.id for parent in self.ou] - account = [ - account for account in self.accounts - if account.id == kwargs['AccountId'] - ].pop(0) - assert new_parent_id in all_parent_id - assert account.parent_id == kwargs['SourceParentId'] + new_parent_id = self.validate_parent_id(kwargs['DestinationParentId']) + self.validate_parent_id(kwargs['SourceParentId']) + account = self.get_account_by_id(kwargs['AccountId']) index = self.accounts.index(account) self.accounts[index].parent_id = new_parent_id def list_parents(self, **kwargs): if re.compile(r'[0-9]{12}').match(kwargs['ChildId']): - obj_list = self.accounts + child_object = self.get_account_by_id(kwargs['ChildId']) else: - obj_list = self.ou - parent_id = [ - obj.parent_id for obj in obj_list - if obj.id == kwargs['ChildId'] - ].pop(0) + child_object = self.get_organizational_unit_by_id(kwargs['ChildId']) return dict( Parents=[ { @@ -241,17 +261,21 @@ class OrganizationsBackend(BaseBackend): 'Type': ou.type, } for ou in self.ou - if ou.id == parent_id + if ou.id == child_object.parent_id ] ) def list_children(self, **kwargs): + parent_id = self.validate_parent_id(kwargs['ParentId']) if kwargs['ChildType'] == 'ACCOUNT': obj_list = self.accounts elif kwargs['ChildType'] == 'ORGANIZATIONAL_UNIT': obj_list = self.ou else: - raise ValueError + raise RESTError( + 'InvalidInputException', + 'You specified an invalid value.' + ) return dict( Children=[ { @@ -259,7 +283,7 @@ class OrganizationsBackend(BaseBackend): 'Type': kwargs['ChildType'], } for obj in obj_list - if obj.parent_id == kwargs['ParentId'] + if obj.parent_id == parent_id ] ) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index c2f06774a..ae9bacd82 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -2,9 +2,11 @@ from __future__ import unicode_literals import boto3 import sure # noqa -import yaml +from botocore.exceptions import ClientError +from nose.tools import assert_raises from moto import mock_organizations +from moto.organizations import utils from .organizations_test_utils import ( validate_organization, validate_roots, @@ -67,6 +69,20 @@ def test_describe_organizational_unit(): validate_organizational_unit(org, response) +@mock_organizations +def test_describe_organizational_unit_exception(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + with assert_raises(ClientError) as e: + response = client.describe_organizational_unit( + OrganizationalUnitId=utils.make_random_root_id() + ) + ex = e.exception + ex.operation_name.should.equal('DescribeOrganizationalUnit') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('OrganizationalUnitNotFoundException') + + @mock_organizations def test_list_organizational_units_for_parent(): client = boto3.client('organizations', region_name='us-east-1') @@ -81,6 +97,19 @@ def test_list_organizational_units_for_parent(): validate_organizational_unit(org, dict(OrganizationalUnit=ou)) +@mock_organizations +def test_list_organizational_units_for_parent_exception(): + client = boto3.client('organizations', region_name='us-east-1') + with assert_raises(ClientError) as e: + response = client.list_organizational_units_for_parent( + ParentId=utils.make_random_root_id() + ) + ex = e.exception + ex.operation_name.should.equal('ListOrganizationalUnitsForParent') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('ParentNotFoundException') + + # Accounts mockname = 'mock-account' mockdomain = 'moto-example.org' @@ -111,6 +140,17 @@ def test_describe_account(): response['Account']['Email'].should.equal(mockemail) +@mock_organizations +def test_describe_account_exception(): + client = boto3.client('organizations', region_name='us-east-1') + with assert_raises(ClientError) as e: + response = client.describe_account(AccountId=utils.make_random_account_id()) + ex = e.exception + ex.operation_name.should.equal('DescribeAccount') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('AccountNotFoundException') + + @mock_organizations def test_list_accounts(): client = boto3.client('organizations', region_name='us-east-1') @@ -211,7 +251,7 @@ def test_list_parents_for_accounts(): @mock_organizations -def test_list_chidlren(): +def test_list_children(): client = boto3.client('organizations', region_name='us-east-1') org = client.create_organization(FeatureSet='ALL')['Organization'] root_id = client.list_roots()['Roots'][0]['Id'] @@ -244,3 +284,28 @@ def test_list_chidlren(): response03['Children'][0]['Type'].should.equal('ACCOUNT') response04['Children'][0]['Id'].should.equal(ou02_id) response04['Children'][0]['Type'].should.equal('ORGANIZATIONAL_UNIT') + + +@mock_organizations +def test_list_children_exception(): + client = boto3.client('organizations', region_name='us-east-1') + org = client.create_organization(FeatureSet='ALL')['Organization'] + root_id = client.list_roots()['Roots'][0]['Id'] + with assert_raises(ClientError) as e: + response = client.list_children( + ParentId=utils.make_random_root_id(), + ChildType='ACCOUNT' + ) + ex = e.exception + ex.operation_name.should.equal('ListChildren') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('ParentNotFoundException') + with assert_raises(ClientError) as e: + response = client.list_children( + ParentId=root_id, + ChildType='BLEE' + ) + ex = e.exception + ex.operation_name.should.equal('ListChildren') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('InvalidInputException') From 4356e951e10795fcb9b73ac6b67540fbfeda2001 Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Fri, 20 Jul 2018 14:07:04 -0700 Subject: [PATCH 14/16] [issue #1720] Add support for AWS Organizations fix travis build error --- moto/organizations/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 91896f537..c02b1a492 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -181,7 +181,7 @@ class OrganizationsBackend(BaseBackend): def validate_parent_id(self, parent_id): try: self.get_organizational_unit_by_id(parent_id) - except RESTError as e: + except RESTError: raise RESTError( 'ParentNotFoundException', "You specified parent that doesn't exist." From b8be517be08c5d7b42315ac1af7da23d978d5f9f Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Thu, 26 Jul 2018 12:20:43 -0700 Subject: [PATCH 15/16] organizations support: add exception handling for describe_organizations --- moto/organizations/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/moto/organizations/models.py b/moto/organizations/models.py index c02b1a492..9d5fe3886 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -157,6 +157,11 @@ class OrganizationsBackend(BaseBackend): return self.org.describe() def describe_organization(self): + if not self.org: + raise RESTError( + 'AWSOrganizationsNotInUseException', + "Your account is not a member of an organization." + ) return self.org.describe() def list_roots(self): From 7a88e634eb0e7780a07645faad5545ce2689531d Mon Sep 17 00:00:00 2001 From: Ashley Gould Date: Mon, 8 Oct 2018 15:27:19 -0700 Subject: [PATCH 16/16] organizations: add exception test for describe_organization endpoint --- tests/test_organizations/test_organizations_boto3.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index ae9bacd82..dfac5feeb 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -32,6 +32,17 @@ def test_describe_organization(): validate_organization(response) +@mock_organizations +def test_describe_organization_exception(): + client = boto3.client('organizations', region_name='us-east-1') + with assert_raises(ClientError) as e: + response = client.describe_organization() + ex = e.exception + ex.operation_name.should.equal('DescribeOrganization') + ex.response['Error']['Code'].should.equal('400') + ex.response['Error']['Message'].should.contain('AWSOrganizationsNotInUseException') + + # Organizational Units @mock_organizations