From 8162947ebb320c769b951ead443e31acc33c967f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Fri, 31 Jul 2020 17:32:57 +0200 Subject: [PATCH] Organizations - implement Delegated Administrator functionality (#3200) * Add organizations.register_delegated_administrator * Add organizations.list_delegated_administrators * Add organizations.list_delegated_services_for_account * Add organizations.deregister_delegated_administrator * Fix Python2 incompatibility --- moto/organizations/exceptions.py | 48 +++ moto/organizations/models.py | 207 +++++++-- moto/organizations/responses.py | 28 ++ .../test_organizations_boto3.py | 394 +++++++++++++++++- 4 files changed, 619 insertions(+), 58 deletions(-) diff --git a/moto/organizations/exceptions.py b/moto/organizations/exceptions.py index 036eeccbc..2d1ee7328 100644 --- a/moto/organizations/exceptions.py +++ b/moto/organizations/exceptions.py @@ -2,6 +2,54 @@ from __future__ import unicode_literals from moto.core.exceptions import JsonRESTError +class AccountAlreadyRegisteredException(JsonRESTError): + code = 400 + + def __init__(self): + super(AccountAlreadyRegisteredException, self).__init__( + "AccountAlreadyRegisteredException", + "The provided account is already a delegated administrator for your organization.", + ) + + +class AccountNotRegisteredException(JsonRESTError): + code = 400 + + def __init__(self): + super(AccountNotRegisteredException, self).__init__( + "AccountNotRegisteredException", + "The provided account is not a registered delegated administrator for your organization.", + ) + + +class AccountNotFoundException(JsonRESTError): + code = 400 + + def __init__(self): + super(AccountNotFoundException, self).__init__( + "AccountNotFoundException", "You specified an account that doesn't exist." + ) + + +class AWSOrganizationsNotInUseException(JsonRESTError): + code = 400 + + def __init__(self): + super(AWSOrganizationsNotInUseException, self).__init__( + "AWSOrganizationsNotInUseException", + "Your account is not a member of an organization.", + ) + + +class ConstraintViolationException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(ConstraintViolationException, self).__init__( + "ConstraintViolationException", message + ) + + class InvalidInputException(JsonRESTError): code = 400 diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 6c1dab15d..6c8029e3d 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -4,7 +4,7 @@ import datetime import re import json -from moto.core import BaseBackend, BaseModel +from moto.core import BaseBackend, BaseModel, ACCOUNT_ID from moto.core.exceptions import RESTError from moto.core.utils import unix_time from moto.organizations import utils @@ -12,6 +12,11 @@ from moto.organizations.exceptions import ( InvalidInputException, DuplicateOrganizationalUnitException, DuplicatePolicyException, + AccountNotFoundException, + ConstraintViolationException, + AccountAlreadyRegisteredException, + AWSOrganizationsNotInUseException, + AccountNotRegisteredException, ) @@ -85,15 +90,13 @@ class FakeAccount(BaseModel): def describe(self): return { - "Account": { - "Id": self.id, - "Arn": self.arn, - "Email": self.email, - "Name": self.name, - "Status": self.status, - "JoinedMethod": self.joined_method, - "JoinedTimestamp": unix_time(self.create_time), - } + "Id": self.id, + "Arn": self.arn, + "Email": self.email, + "Name": self.name, + "Status": self.status, + "JoinedMethod": self.joined_method, + "JoinedTimestamp": unix_time(self.create_time), } @@ -221,6 +224,56 @@ class FakeServiceAccess(BaseModel): return service_principal in FakeServiceAccess.TRUSTED_SERVICES +class FakeDelegatedAdministrator(BaseModel): + # List of services, which support a different Account to ba a delegated administrator + # https://docs.aws.amazon.com/organizations/latest/userguide/orgs_integrated-services-list.html + SUPPORTED_SERVICES = [ + "config-multiaccountsetup.amazonaws.com", + "guardduty.amazonaws.com", + "access-analyzer.amazonaws.com", + "macie.amazonaws.com", + "servicecatalog.amazonaws.com", + "ssm.amazonaws.com", + ] + + def __init__(self, account): + self.account = account + self.enabled_date = datetime.datetime.utcnow() + self.services = {} + + def add_service_principal(self, service_principal): + if service_principal in self.services: + raise AccountAlreadyRegisteredException + + if not self.supported_service(service_principal): + raise InvalidInputException( + "You specified an unrecognized service principal." + ) + + self.services[service_principal] = { + "ServicePrincipal": service_principal, + "DelegationEnabledDate": unix_time(datetime.datetime.utcnow()), + } + + def remove_service_principal(self, service_principal): + if service_principal not in self.services: + raise InvalidInputException( + "You specified an unrecognized service principal." + ) + + self.services.pop(service_principal) + + def describe(self): + admin = self.account.describe() + admin["DelegationEnabledDate"] = unix_time(self.enabled_date) + + return admin + + @staticmethod + def supported_service(service_principal): + return service_principal in FakeDelegatedAdministrator.SUPPORTED_SERVICES + + class OrganizationsBackend(BaseBackend): def __init__(self): self.org = None @@ -228,6 +281,7 @@ class OrganizationsBackend(BaseBackend): self.ou = [] self.policies = [] self.services = [] + self.admins = [] def create_organization(self, **kwargs): self.org = FakeOrganization(kwargs["FeatureSet"]) @@ -259,10 +313,7 @@ class OrganizationsBackend(BaseBackend): def describe_organization(self): if not self.org: - raise RESTError( - "AWSOrganizationsNotInUseException", - "Your account is not a member of an organization.", - ) + raise AWSOrganizationsNotInUseException return self.org.describe() def list_roots(self): @@ -325,10 +376,7 @@ class OrganizationsBackend(BaseBackend): (account for account in self.accounts if account.id == account_id), None ) if account is None: - raise RESTError( - "AccountNotFoundException", - "You specified an account that doesn't exist.", - ) + raise AccountNotFoundException return account def get_account_by_attr(self, attr, value): @@ -341,15 +389,12 @@ class OrganizationsBackend(BaseBackend): None, ) if account is None: - raise RESTError( - "AccountNotFoundException", - "You specified an account that doesn't exist.", - ) + raise AccountNotFoundException return account def describe_account(self, **kwargs): account = self.get_account_by_id(kwargs["AccountId"]) - return account.describe() + return dict(Account=account.describe()) def describe_create_account_status(self, **kwargs): account = self.get_account_by_attr( @@ -358,15 +403,13 @@ class OrganizationsBackend(BaseBackend): return account.create_account_status def list_accounts(self): - return dict( - Accounts=[account.describe()["Account"] for account in self.accounts] - ) + return dict(Accounts=[account.describe() for account in self.accounts]) def list_accounts_for_parent(self, **kwargs): parent_id = self.validate_parent_id(kwargs["ParentId"]) return dict( Accounts=[ - account.describe()["Account"] + account.describe() for account in self.accounts if account.parent_id == parent_id ] @@ -399,7 +442,7 @@ class OrganizationsBackend(BaseBackend): elif kwargs["ChildType"] == "ORGANIZATIONAL_UNIT": obj_list = self.ou else: - raise RESTError("InvalidInputException", "You specified an invalid value.") + raise InvalidInputException("You specified an invalid value.") return dict( Children=[ {"Id": obj.id, "Type": kwargs["ChildType"]} @@ -427,7 +470,7 @@ class OrganizationsBackend(BaseBackend): "You specified a policy that doesn't exist.", ) else: - raise RESTError("InvalidInputException", "You specified an invalid value.") + raise InvalidInputException("You specified an invalid value.") return policy.describe() def get_policy_by_id(self, policy_id): @@ -472,12 +515,9 @@ class OrganizationsBackend(BaseBackend): account.attached_policies.append(policy) policy.attachments.append(account) else: - raise RESTError( - "AccountNotFoundException", - "You specified an account that doesn't exist.", - ) + raise AccountNotFoundException else: - raise RESTError("InvalidInputException", "You specified an invalid value.") + raise InvalidInputException("You specified an invalid value.") def list_policies(self, **kwargs): return dict( @@ -510,12 +550,9 @@ class OrganizationsBackend(BaseBackend): elif re.compile(utils.ACCOUNT_ID_REGEX).match(kwargs["TargetId"]): obj = next((a for a in self.accounts if a.id == kwargs["TargetId"]), None) if obj is None: - raise RESTError( - "AccountNotFoundException", - "You specified an account that doesn't exist.", - ) + raise AccountNotFoundException else: - raise RESTError("InvalidInputException", "You specified an invalid value.") + raise InvalidInputException("You specified an invalid value.") return dict( Policies=[ p.describe()["Policy"]["PolicySummary"] for p in obj.attached_policies @@ -533,7 +570,7 @@ class OrganizationsBackend(BaseBackend): "You specified a policy that doesn't exist.", ) else: - raise RESTError("InvalidInputException", "You specified an invalid value.") + raise InvalidInputException("You specified an invalid value.") objects = [ {"TargetId": obj.id, "Arn": obj.arn, "Name": obj.name, "Type": obj.type} for obj in policy.attachments @@ -606,5 +643,95 @@ class OrganizationsBackend(BaseBackend): if service_principal: self.services.remove(service_principal) + def register_delegated_administrator(self, **kwargs): + account_id = kwargs["AccountId"] + + if account_id == ACCOUNT_ID: + raise ConstraintViolationException( + "You cannot register master account/yourself as delegated administrator for your organization." + ) + + account = self.get_account_by_id(account_id) + + admin = next( + (admin for admin in self.admins if admin.account.id == account_id), None + ) + if admin is None: + admin = FakeDelegatedAdministrator(account) + self.admins.append(admin) + + admin.add_service_principal(kwargs["ServicePrincipal"]) + + def list_delegated_administrators(self, **kwargs): + admins = self.admins + service = kwargs.get("ServicePrincipal") + + if service: + if not FakeDelegatedAdministrator.supported_service(service): + raise InvalidInputException( + "You specified an unrecognized service principal." + ) + + admins = [admin for admin in admins if service in admin.services] + + delegated_admins = [admin.describe() for admin in admins] + + return dict(DelegatedAdministrators=delegated_admins) + + def list_delegated_services_for_account(self, **kwargs): + admin = next( + (admin for admin in self.admins if admin.account.id == kwargs["AccountId"]), + None, + ) + if admin is None: + account = next( + ( + account + for account in self.accounts + if account.id == kwargs["AccountId"] + ), + None, + ) + if account: + raise AccountNotRegisteredException + + raise AWSOrganizationsNotInUseException + + services = [service for service in admin.services.values()] + + return dict(DelegatedServices=services) + + def deregister_delegated_administrator(self, **kwargs): + account_id = kwargs["AccountId"] + service = kwargs["ServicePrincipal"] + + if account_id == ACCOUNT_ID: + raise ConstraintViolationException( + "You cannot register master account/yourself as delegated administrator for your organization." + ) + + admin = next( + (admin for admin in self.admins if admin.account.id == account_id), None, + ) + if admin is None: + account = next( + ( + account + for account in self.accounts + if account.id == kwargs["AccountId"] + ), + None, + ) + if account: + raise AccountNotRegisteredException + + raise AccountNotFoundException + + admin.remove_service_principal(service) + + # remove account, when no services attached + if not admin.services: + self.admins.remove(admin) + organizations_backend = OrganizationsBackend() diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index a2bd028d9..4689db5d7 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -163,3 +163,31 @@ class OrganizationsResponse(BaseResponse): return json.dumps( self.organizations_backend.disable_aws_service_access(**self.request_params) ) + + def register_delegated_administrator(self): + return json.dumps( + self.organizations_backend.register_delegated_administrator( + **self.request_params + ) + ) + + def list_delegated_administrators(self): + return json.dumps( + self.organizations_backend.list_delegated_administrators( + **self.request_params + ) + ) + + def list_delegated_services_for_account(self): + return json.dumps( + self.organizations_backend.list_delegated_services_for_account( + **self.request_params + ) + ) + + def deregister_delegated_administrator(self): + return json.dumps( + self.organizations_backend.deregister_delegated_administrator( + **self.request_params + ) + ) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index decc0a178..90bee1edb 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -10,6 +10,7 @@ from botocore.exceptions import ClientError from nose.tools import assert_raises from moto import mock_organizations +from moto.core import ACCOUNT_ID from moto.organizations import utils from .organizations_test_utils import ( validate_organization, @@ -64,8 +65,11 @@ def test_describe_organization_exception(): 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") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AWSOrganizationsNotInUseException") + ex.response["Error"]["Message"].should.equal( + "Your account is not a member of an organization." + ) # Organizational Units @@ -193,8 +197,11 @@ def test_describe_account_exception(): 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") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountNotFoundException") + ex.response["Error"]["Message"].should.equal( + "You specified an account that doesn't exist." + ) @mock_organizations @@ -340,8 +347,9 @@ def test_list_children_exception(): 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") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal("You specified an invalid value.") # Service Control Policies @@ -405,8 +413,9 @@ def test_describe_policy_exception(): response = client.describe_policy(PolicyId="meaninglessstring") ex = e.exception ex.operation_name.should.equal("DescribePolicy") - ex.response["Error"]["Code"].should.equal("400") - ex.response["Error"]["Message"].should.contain("InvalidInputException") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal("You specified an invalid value.") @mock_organizations @@ -517,16 +526,20 @@ def test_attach_policy_exception(): response = client.attach_policy(PolicyId=policy_id, TargetId=account_id) ex = e.exception ex.operation_name.should.equal("AttachPolicy") - ex.response["Error"]["Code"].should.equal("400") - ex.response["Error"]["Message"].should.contain("AccountNotFoundException") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountNotFoundException") + ex.response["Error"]["Message"].should.equal( + "You specified an account that doesn't exist." + ) with assert_raises(ClientError) as e: response = client.attach_policy( PolicyId=policy_id, TargetId="meaninglessstring" ) ex = e.exception ex.operation_name.should.equal("AttachPolicy") - ex.response["Error"]["Code"].should.equal("400") - ex.response["Error"]["Message"].should.contain("InvalidInputException") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal("You specified an invalid value.") @mock_organizations @@ -636,16 +649,20 @@ def test_list_policies_for_target_exception(): ) ex = e.exception ex.operation_name.should.equal("ListPoliciesForTarget") - ex.response["Error"]["Code"].should.equal("400") - ex.response["Error"]["Message"].should.contain("AccountNotFoundException") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountNotFoundException") + ex.response["Error"]["Message"].should.equal( + "You specified an account that doesn't exist." + ) with assert_raises(ClientError) as e: response = client.list_policies_for_target( TargetId="meaninglessstring", Filter="SERVICE_CONTROL_POLICY" ) ex = e.exception ex.operation_name.should.equal("ListPoliciesForTarget") - ex.response["Error"]["Code"].should.equal("400") - ex.response["Error"]["Message"].should.contain("InvalidInputException") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal("You specified an invalid value.") @mock_organizations @@ -694,8 +711,9 @@ def test_list_targets_for_policy_exception(): response = client.list_targets_for_policy(PolicyId="meaninglessstring") ex = e.exception ex.operation_name.should.equal("ListTargetsForPolicy") - ex.response["Error"]["Code"].should.equal("400") - ex.response["Error"]["Message"].should.contain("InvalidInputException") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal("You specified an invalid value.") @mock_organizations @@ -947,3 +965,343 @@ def test_disable_aws_service_access_errors(): ex.response["Error"]["Message"].should.equal( "You specified an unrecognized service principal." ) + + +@mock_organizations +def test_register_delegated_administrator(): + # given + client = boto3.client("organizations", region_name="us-east-1") + org_id = client.create_organization(FeatureSet="ALL")["Organization"]["Id"] + account_id = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["AccountId"] + + # when + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + + # then + response = client.list_delegated_administrators() + response["DelegatedAdministrators"].should.have.length_of(1) + admin = response["DelegatedAdministrators"][0] + admin["Id"].should.equal(account_id) + admin["Arn"].should.equal( + "arn:aws:organizations::{0}:account/{1}/{2}".format( + ACCOUNT_ID, org_id, account_id + ) + ) + admin["Email"].should.equal(mockemail) + admin["Name"].should.equal(mockname) + admin["Status"].should.equal("ACTIVE") + admin["JoinedMethod"].should.equal("CREATED") + admin["JoinedTimestamp"].should.be.a(datetime) + admin["DelegationEnabledDate"].should.be.a(datetime) + + +@mock_organizations +def test_register_delegated_administrator_errors(): + # given + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL") + account_id = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["AccountId"] + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + + # register master Account + # when + with assert_raises(ClientError) as e: + client.register_delegated_administrator( + AccountId=ACCOUNT_ID, ServicePrincipal="ssm.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("RegisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ConstraintViolationException") + ex.response["Error"]["Message"].should.equal( + "You cannot register master account/yourself as delegated administrator for your organization." + ) + + # register not existing Account + # when + with assert_raises(ClientError) as e: + client.register_delegated_administrator( + AccountId="000000000000", ServicePrincipal="ssm.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("RegisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountNotFoundException") + ex.response["Error"]["Message"].should.equal( + "You specified an account that doesn't exist." + ) + + # register not supported service + # when + with assert_raises(ClientError) as e: + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="moto.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("RegisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal( + "You specified an unrecognized service principal." + ) + + # register service again + # when + with assert_raises(ClientError) as e: + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("RegisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountAlreadyRegisteredException") + ex.response["Error"]["Message"].should.equal( + "The provided account is already a delegated administrator for your organization." + ) + + +@mock_organizations +def test_list_delegated_administrators(): + # given + client = boto3.client("organizations", region_name="us-east-1") + org_id = client.create_organization(FeatureSet="ALL")["Organization"]["Id"] + account_id_1 = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["AccountId"] + account_id_2 = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["AccountId"] + client.register_delegated_administrator( + AccountId=account_id_1, ServicePrincipal="ssm.amazonaws.com" + ) + client.register_delegated_administrator( + AccountId=account_id_2, ServicePrincipal="guardduty.amazonaws.com" + ) + + # when + response = client.list_delegated_administrators() + + # then + response["DelegatedAdministrators"].should.have.length_of(2) + sorted([admin["Id"] for admin in response["DelegatedAdministrators"]]).should.equal( + sorted([account_id_1, account_id_2]) + ) + + # when + response = client.list_delegated_administrators( + ServicePrincipal="ssm.amazonaws.com" + ) + + # then + response["DelegatedAdministrators"].should.have.length_of(1) + admin = response["DelegatedAdministrators"][0] + admin["Id"].should.equal(account_id_1) + admin["Arn"].should.equal( + "arn:aws:organizations::{0}:account/{1}/{2}".format( + ACCOUNT_ID, org_id, account_id_1 + ) + ) + admin["Email"].should.equal(mockemail) + admin["Name"].should.equal(mockname) + admin["Status"].should.equal("ACTIVE") + admin["JoinedMethod"].should.equal("CREATED") + admin["JoinedTimestamp"].should.be.a(datetime) + admin["DelegationEnabledDate"].should.be.a(datetime) + + +@mock_organizations +def test_list_delegated_administrators_erros(): + # given + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL") + + # list not supported service + # when + with assert_raises(ClientError) as e: + client.list_delegated_administrators(ServicePrincipal="moto.amazonaws.com") + + # then + ex = e.exception + ex.operation_name.should.equal("ListDelegatedAdministrators") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal( + "You specified an unrecognized service principal." + ) + + +@mock_organizations +def test_list_delegated_services_for_account(): + # given + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL") + account_id = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["AccountId"] + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="guardduty.amazonaws.com" + ) + + # when + response = client.list_delegated_services_for_account(AccountId=account_id) + + # then + response["DelegatedServices"].should.have.length_of(2) + sorted( + [service["ServicePrincipal"] for service in response["DelegatedServices"]] + ).should.equal(["guardduty.amazonaws.com", "ssm.amazonaws.com"]) + + +@mock_organizations +def test_list_delegated_services_for_account_erros(): + # given + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL") + + # list services for not existing Account + # when + with assert_raises(ClientError) as e: + client.list_delegated_services_for_account(AccountId="000000000000") + + # then + ex = e.exception + ex.operation_name.should.equal("ListDelegatedServicesForAccount") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AWSOrganizationsNotInUseException") + ex.response["Error"]["Message"].should.equal( + "Your account is not a member of an organization." + ) + + # list services for not registered Account + # when + with assert_raises(ClientError) as e: + client.list_delegated_services_for_account(AccountId=ACCOUNT_ID) + + # then + ex = e.exception + ex.operation_name.should.equal("ListDelegatedServicesForAccount") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountNotRegisteredException") + ex.response["Error"]["Message"].should.equal( + "The provided account is not a registered delegated administrator for your organization." + ) + + +@mock_organizations +def test_deregister_delegated_administrator(): + # given + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL") + account_id = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["AccountId"] + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + + # when + client.deregister_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + + # then + response = client.list_delegated_administrators() + response["DelegatedAdministrators"].should.have.length_of(0) + + +@mock_organizations +def test_deregister_delegated_administrator_erros(): + # given + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL") + account_id = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["AccountId"] + + # deregister master Account + # when + with assert_raises(ClientError) as e: + client.deregister_delegated_administrator( + AccountId=ACCOUNT_ID, ServicePrincipal="ssm.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("DeregisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("ConstraintViolationException") + ex.response["Error"]["Message"].should.equal( + "You cannot register master account/yourself as delegated administrator for your organization." + ) + + # deregister not existing Account + # when + with assert_raises(ClientError) as e: + client.deregister_delegated_administrator( + AccountId="000000000000", ServicePrincipal="ssm.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("DeregisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountNotFoundException") + ex.response["Error"]["Message"].should.equal( + "You specified an account that doesn't exist." + ) + + # deregister not registered Account + # when + with assert_raises(ClientError) as e: + client.deregister_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("DeregisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("AccountNotRegisteredException") + ex.response["Error"]["Message"].should.equal( + "The provided account is not a registered delegated administrator for your organization." + ) + + # given + client.register_delegated_administrator( + AccountId=account_id, ServicePrincipal="ssm.amazonaws.com" + ) + + # deregister not registered service + # when + with assert_raises(ClientError) as e: + client.deregister_delegated_administrator( + AccountId=account_id, ServicePrincipal="guardduty.amazonaws.com" + ) + + # then + ex = e.exception + ex.operation_name.should.equal("DeregisterDelegatedAdministrator") + ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.response["Error"]["Code"].should.contain("InvalidInputException") + ex.response["Error"]["Message"].should.equal( + "You specified an unrecognized service principal." + )