diff --git a/moto/greengrass/models.py b/moto/greengrass/models.py index baaa701ae..9a395c375 100644 --- a/moto/greengrass/models.py +++ b/moto/greengrass/models.py @@ -361,10 +361,25 @@ class FakeGroupVersion(BaseModel): return obj +class FakeAssociatedRole(BaseModel): + def __init__(self, role_arn): + self.role_arn = role_arn + self.associated_at = datetime.utcnow() + + def to_dict(self, include_detail=False): + + obj = {"AssociatedAt": iso_8601_datetime_with_milliseconds(self.associated_at)} + if include_detail: + obj["RoleArn"] = self.role_arn + + return obj + + class GreengrassBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) self.groups = OrderedDict() + self.group_role_associations = OrderedDict() self.group_versions = OrderedDict() self.core_definitions = OrderedDict() self.core_definition_versions = OrderedDict() @@ -1066,5 +1081,28 @@ class GreengrassBackend(BaseBackend): return self.group_versions[group_id][group_version_id] + def associate_role_to_group(self, group_id, role_arn): + + # I don't know why, AssociateRoleToGroup does not check specified group is exists + # So, this API allows any group id such as "a" + + associated_role = FakeAssociatedRole(role_arn) + self.group_role_associations[group_id] = associated_role + return associated_role + + def get_associated_role(self, group_id): + + if group_id not in self.group_role_associations: + raise GreengrassClientError( + "404", "You need to attach an IAM role to this deployment group." + ) + + return self.group_role_associations[group_id] + + def disassociate_role_from_group(self, group_id): + if group_id not in self.group_role_associations: + return + del self.group_role_associations[group_id] + greengrass_backends = BackendDict(GreengrassBackend, "greengrass") diff --git a/moto/greengrass/responses.py b/moto/greengrass/responses.py index 78b993d36..1601d9be4 100644 --- a/moto/greengrass/responses.py +++ b/moto/greengrass/responses.py @@ -1,5 +1,7 @@ +from datetime import datetime import json +from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.core.responses import BaseResponse from .models import greengrass_backends @@ -677,3 +679,50 @@ class GreengrassResponse(BaseResponse): group_version_id=group_version_id, ) return 200, {"status": 200}, json.dumps(res.to_dict(include_detail=True)) + + def role(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == "PUT": + return self.associate_role_to_group() + + if self.method == "GET": + return self.get_associated_role() + + if self.method == "DELETE": + return self.disassociate_role_from_group() + + def associate_role_to_group(self): + + group_id = self.path.split("/")[-2] + role_arn = self._get_param("RoleArn") + res = self.greengrass_backend.associate_role_to_group( + group_id=group_id, + role_arn=role_arn, + ) + return 200, {"status": 200}, json.dumps(res.to_dict()) + + def get_associated_role(self): + + group_id = self.path.split("/")[-2] + res = self.greengrass_backend.get_associated_role( + group_id=group_id, + ) + return 200, {"status": 200}, json.dumps(res.to_dict(include_detail=True)) + + def disassociate_role_from_group(self): + group_id = self.path.split("/")[-2] + self.greengrass_backend.disassociate_role_from_group( + group_id=group_id, + ) + return ( + 200, + {"status": 200}, + json.dumps( + { + "DisassociatedAt": iso_8601_datetime_with_milliseconds( + datetime.utcnow() + ) + } + ), + ) diff --git a/moto/greengrass/urls.py b/moto/greengrass/urls.py index 6583c6aef..653fc752e 100644 --- a/moto/greengrass/urls.py +++ b/moto/greengrass/urls.py @@ -31,6 +31,7 @@ url_paths = { "{0}/greengrass/definition/resources/(?P[^/]+)/versions/(?P[^/]+)/?$": response.resource_definition_version, "{0}/greengrass/groups$": response.groups, "{0}/greengrass/groups/(?P[^/]+)/?$": response.group, + "{0}/greengrass/groups/(?P[^/]+)/role$": response.role, "{0}/greengrass/groups/(?P[^/]+)/versions$": response.group_versions, "{0}/greengrass/groups/(?P[^/]+)/versions/(?P[^/]+)/?$": response.group_version, } diff --git a/tests/test_greengrass/test_greengrass_groups.py b/tests/test_greengrass/test_greengrass_groups.py index 74a04eeef..4d345ea08 100644 --- a/tests/test_greengrass/test_greengrass_groups.py +++ b/tests/test_greengrass/test_greengrass_groups.py @@ -473,3 +473,88 @@ def test_get_group_version_with_invalid_version_id(): f"Version {invalid_group_ver_id} of Group Definition {group_id} does not exist." ) ex.value.response["Error"]["Code"].should.equal("VersionNotFoundException") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_associate_role_to_group(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + res = client.associate_role_to_group( + GroupId="abc002c8-1093-485e-9324-3baadf38e582", + RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/greengrass-role", + ) + + res.should.have.key("AssociatedAt") + res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_get_associated_role(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + group_id = "abc002c8-1093-485e-9324-3baadf38e582" + role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/greengrass-role" + client.associate_role_to_group(GroupId=group_id, RoleArn=role_arn) + + res = client.get_associated_role(GroupId=group_id) + res.should.have.key("AssociatedAt") + res.should.have.key("RoleArn").should.equal(role_arn) + res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + if not TEST_SERVER_MODE: + res["AssociatedAt"].should.equal("2022-06-01T12:00:00.000Z") + + +@mock_greengrass +def test_get_associated_role_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + with pytest.raises(ClientError) as ex: + client.get_associated_role(GroupId="abc002c8-1093-485e-9324-3baadf38e582") + + ex.value.response["Error"]["Message"].should.equal( + "You need to attach an IAM role to this deployment group." + ) + ex.value.response["Error"]["Code"].should.equal("404") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_disassociate_role_from_group(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + group_id = "abc002c8-1093-485e-9324-3baadf38e582" + role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/greengrass-role" + client.associate_role_to_group(GroupId=group_id, RoleArn=role_arn) + client.get_associated_role(GroupId=group_id) + + res = client.disassociate_role_from_group(GroupId=group_id) + res.should.have.key("DisassociatedAt") + res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + if not TEST_SERVER_MODE: + res["DisassociatedAt"].should.equal("2022-06-01T12:00:00.000Z") + + with pytest.raises(ClientError) as ex: + client.get_associated_role(GroupId=group_id) + + ex.value.response["Error"]["Message"].should.equal( + "You need to attach an IAM role to this deployment group." + ) + ex.value.response["Error"]["Code"].should.equal("404") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_disassociate_role_from_group_with_none_exists_group_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + group_id = "abc002c8-1093-485e-9324-3baadf38e582" + res = client.disassociate_role_from_group(GroupId=group_id) + res.should.have.key("DisassociatedAt") + res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + if not TEST_SERVER_MODE: + res["DisassociatedAt"].should.equal("2022-06-01T12:00:00.000Z")