diff --git a/moto/ec2/models.py b/moto/ec2/models.py
index a83284ef6..b4d491264 100644
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -820,6 +820,7 @@ class Ami(TaggedEC2Resource):
self.description = description if description else source_ami.description
self.launch_permission_groups = set()
+ self.launch_permission_users = set()
# AWS auto-creates these, we should reflect the same.
volume = self.ec2_backend.create_volume(15, "us-east-1a")
@@ -902,24 +903,49 @@ class AmiBackend(object):
ami = self.describe_images(ami_ids=[ami_id])[0]
return ami.launch_permission_groups
- def add_launch_permission(self, ami_id, user_id=None, group=None):
- if user_id:
- self.raise_not_implemented_error("The UserId parameter for ModifyImageAttribute")
-
- if group != 'all':
- raise InvalidAMIAttributeItemValueError("UserGroup", group)
+ def get_launch_permission_users(self, ami_id):
ami = self.describe_images(ami_ids=[ami_id])[0]
- ami.launch_permission_groups.add(group)
+ return ami.launch_permission_users
+
+ def validate_permission_targets(self, user_ids=None, group=None):
+ # If anything is invalid, nothing is added. (No partial success.)
+ if user_ids:
+ """
+ AWS docs:
+ "The AWS account ID is a 12-digit number, such as 123456789012, that you use to construct Amazon Resource Names (ARNs)."
+ http://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
+ """
+ for user_id in user_ids:
+ if len(user_id) != 12 or not user_id.isdigit():
+ raise InvalidAMIAttributeItemValueError("userId", user_id)
+
+ if group and group != 'all':
+ raise InvalidAMIAttributeItemValueError("UserGroup", group)
+
+ def add_launch_permission(self, ami_id, user_ids=None, group=None):
+ ami = self.describe_images(ami_ids=[ami_id])[0]
+ self.validate_permission_targets(user_ids=user_ids, group=group)
+
+ if user_ids:
+ for user_id in user_ids:
+ ami.launch_permission_users.add(user_id)
+
+ if group:
+ ami.launch_permission_groups.add(group)
+
return True
- def remove_launch_permission(self, ami_id, user_id=None, group=None):
- if user_id:
- self.raise_not_implemented_error("The UserId parameter for ModifyImageAttribute")
-
- if group != 'all':
- raise InvalidAMIAttributeItemValueError("UserGroup", group)
+ def remove_launch_permission(self, ami_id, user_ids=None, group=None):
ami = self.describe_images(ami_ids=[ami_id])[0]
- ami.launch_permission_groups.discard(group)
+ self.validate_permission_targets(user_ids=user_ids, group=group)
+
+ if user_ids:
+ for user_id in user_ids:
+ ami.launch_permission_users.discard(user_id)
+
+ if group:
+ ami.launch_permission_groups.discard(group)
+
return True
diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py
index 4af2b9be6..7d715fd51 100644
--- a/moto/ec2/responses/amis.py
+++ b/moto/ec2/responses/amis.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from moto.core.responses import BaseResponse
-from moto.ec2.utils import instance_ids_from_querystring, image_ids_from_querystring, filters_from_querystring
+from moto.ec2.utils import instance_ids_from_querystring, image_ids_from_querystring, \
+ filters_from_querystring, sequence_from_querystring
class AmisResponse(BaseResponse):
@@ -41,18 +42,19 @@ class AmisResponse(BaseResponse):
def describe_image_attribute(self):
ami_id = self.querystring.get('ImageId')[0]
groups = self.ec2_backend.get_launch_permission_groups(ami_id)
+ users = self.ec2_backend.get_launch_permission_users(ami_id)
template = self.response_template(DESCRIBE_IMAGE_ATTRIBUTES_RESPONSE)
- return template.render(ami_id=ami_id, groups=groups)
+ return template.render(ami_id=ami_id, groups=groups, users=users)
def modify_image_attribute(self):
ami_id = self.querystring.get('ImageId')[0]
operation_type = self.querystring.get('OperationType')[0]
group = self.querystring.get('UserGroup.1', [None])[0]
- user_id = self.querystring.get('UserId.1', [None])[0]
+ user_ids = sequence_from_querystring('UserId', self.querystring)
if (operation_type == 'add'):
- self.ec2_backend.add_launch_permission(ami_id, user_id=user_id, group=group)
+ self.ec2_backend.add_launch_permission(ami_id, user_ids=user_ids, group=group)
elif (operation_type == 'remove'):
- self.ec2_backend.remove_launch_permission(ami_id, user_id=user_id, group=group)
+ self.ec2_backend.remove_launch_permission(ami_id, user_ids=user_ids, group=group)
return MODIFY_IMAGE_ATTRIBUTE_RESPONSE
def register_image(self):
@@ -140,16 +142,24 @@ DESCRIBE_IMAGE_ATTRIBUTES_RESPONSE = """
59dbff89-35bd-4eac-99ed-be587EXAMPLE
{{ ami_id }}
- {% if not groups %}
+ {% if not groups and not users %}
- {% endif %}
- {% if groups %}
+ {% else %}
- {% for group in groups %}
- -
- {{ group }}
-
- {% endfor %}
+ {% if groups %}
+ {% for group in groups %}
+ -
+ {{ group }}
+
+ {% endfor %}
+ {% endif %}
+ {% if users %}
+ {% for user in users %}
+ -
+ {{ user }}
+
+ {% endfor %}
+ {% endif %}
{% endif %}
"""
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
index 62456bc87..62909e2ea 100644
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -218,7 +218,7 @@ EC2_RUN_INSTANCES = """ length 12.
+ with assert_raises(EC2ResponseError) as cm:
+ conn.modify_image_attribute(image.id,
+ attribute='launchPermission',
+ operation='add',
+ user_ids='1234567890123')
+ cm.exception.code.should.equal('InvalidAMIAttributeItemValue')
+ cm.exception.status.should.equal(400)
+ cm.exception.request_id.should_not.be.none
+
+ # Error: Add with user ID that is < length 12.
+ with assert_raises(EC2ResponseError) as cm:
+ conn.modify_image_attribute(image.id,
+ attribute='launchPermission',
+ operation='add',
+ user_ids='12345678901')
+ cm.exception.code.should.equal('InvalidAMIAttributeItemValue')
+ cm.exception.status.should.equal(400)
+ cm.exception.request_id.should_not.be.none
+
+ # Error: Add with one invalid user ID among other valid IDs, ensure no partial changes.
+ with assert_raises(EC2ResponseError) as cm:
+ conn.modify_image_attribute(image.id,
+ attribute='launchPermission',
+ operation='add',
+ user_ids=['123456789011', 'foo', '123456789022'])
+ cm.exception.code.should.equal('InvalidAMIAttributeItemValue')
+ cm.exception.status.should.equal(400)
+ cm.exception.request_id.should_not.be.none
+
+ attributes = conn.get_image_attribute(image.id, attribute='launchPermission')
+ attributes.attrs.should.have.length_of(0)
+
# Error: Add with invalid image ID
with assert_raises(EC2ResponseError) as cm:
conn.modify_image_attribute("ami-abcd1234",
@@ -313,12 +482,3 @@ def test_ami_attribute():
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
- # Error: Add or remove with user ID instead of group
- conn.modify_image_attribute.when.called_with(image.id,
- attribute='launchPermission',
- operation='add',
- user_ids=['user']).should.throw(NotImplementedError)
- conn.modify_image_attribute.when.called_with(image.id,
- attribute='launchPermission',
- operation='remove',
- user_ids=['user']).should.throw(NotImplementedError)