Repository: getmoto/moto

Issue:
IAM Role support for Permissions Boundary missing
Moto should support the PermissionsBoundary argument of https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html?highlight=create_role#IAM.Client.create_role and should return specified PermissionsBoundary when listing roles.


Discussion:

Resolving patch:
diff --git a/moto/iam/models.py b/moto/iam/models.py
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -9,6 +9,7 @@
 from cryptography.hazmat.backends import default_backend
 
 import pytz
+from moto.core.exceptions import RESTError
 from moto.core import BaseBackend, BaseModel
 from moto.core.utils import iso_8601_datetime_without_milliseconds
 
@@ -131,7 +132,7 @@ class InlinePolicy(Policy):
 
 class Role(BaseModel):
 
-    def __init__(self, role_id, name, assume_role_policy_document, path):
+    def __init__(self, role_id, name, assume_role_policy_document, path, permissions_boundary):
         self.id = role_id
         self.name = name
         self.assume_role_policy_document = assume_role_policy_document
@@ -141,6 +142,7 @@ def __init__(self, role_id, name, assume_role_policy_document, path):
         self.create_date = datetime.now(pytz.utc)
         self.tags = {}
         self.description = ""
+        self.permissions_boundary = permissions_boundary
 
     @classmethod
     def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
@@ -150,6 +152,7 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg
             role_name=resource_name,
             assume_role_policy_document=properties['AssumeRolePolicyDocument'],
             path=properties.get('Path', '/'),
+            permissions_boundary=properties.get('PermissionsBoundary', '')
         )
 
         policies = properties.get('Policies', [])
@@ -470,6 +473,8 @@ def __init__(self):
         self.managed_policies = self._init_managed_policies()
         self.account_aliases = []
         self.saml_providers = {}
+        self.policy_arn_regex = re.compile(
+            r'^arn:aws:iam::[0-9]*:policy/.*$')
         super(IAMBackend, self).__init__()
 
     def _init_managed_policies(self):
@@ -587,9 +592,12 @@ def _filter_attached_policies(self, policies, marker, max_items, path_prefix):
 
         return policies, marker
 
-    def create_role(self, role_name, assume_role_policy_document, path):
+    def create_role(self, role_name, assume_role_policy_document, path, permissions_boundary):
         role_id = random_resource_id()
-        role = Role(role_id, role_name, assume_role_policy_document, path)
+        if permissions_boundary and not self.policy_arn_regex.match(permissions_boundary):
+            raise RESTError('InvalidParameterValue', 'Value ({}) for parameter PermissionsBoundary is invalid.'.format(permissions_boundary))
+
+        role = Role(role_id, role_name, assume_role_policy_document, path, permissions_boundary)
         self.roles[role_id] = role
         return role
 
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -175,9 +175,11 @@ def create_role(self):
         path = self._get_param('Path')
         assume_role_policy_document = self._get_param(
             'AssumeRolePolicyDocument')
+        permissions_boundary = self._get_param(
+            'PermissionsBoundary')
 
         role = iam_backend.create_role(
-            role_name, assume_role_policy_document, path)
+            role_name, assume_role_policy_document, path, permissions_boundary)
         template = self.response_template(CREATE_ROLE_TEMPLATE)
         return template.render(role=role)
 
@@ -1000,6 +1002,12 @@ def untag_role(self):
       <AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
       <CreateDate>{{ role.create_date }}</CreateDate>
       <RoleId>{{ role.id }}</RoleId>
+      {% if role.permissions_boundary %}
+      <PermissionsBoundary>
+          <PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>
+          <PermissionsBoundaryArn>{{ role.permissions_boundary }}</PermissionsBoundaryArn>
+      </PermissionsBoundary>
+      {% endif %}
     </Role>
   </CreateRoleResult>
   <ResponseMetadata>
@@ -1102,6 +1110,12 @@ def untag_role(self):
         <AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
         <CreateDate>{{ role.create_date }}</CreateDate>
         <RoleId>{{ role.id }}</RoleId>
+        {% if role.permissions_boundary %}
+        <PermissionsBoundary>
+          <PermissionsBoundaryType>PermissionsBoundaryPolicy</PermissionsBoundaryType>
+          <PermissionsBoundaryArn>{{ role.permissions_boundary }}</PermissionsBoundaryArn>
+        </PermissionsBoundary>
+        {% endif %}
       </member>
       {% endfor %}
     </Roles>


Test patch:
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -128,7 +128,6 @@ def test_create_role_and_instance_profile():
     profile = conn.create_instance_profile('my-other-profile')
     profile.path.should.equal('/')
 
-
 @mock_iam_deprecated()
 def test_remove_role_from_instance_profile():
     conn = boto.connect_iam()
@@ -358,7 +357,7 @@ def test_list_policy_versions():
     versions = conn.list_policy_versions(
         PolicyArn="arn:aws:iam::123456789012:policy/TestListPolicyVersions")
     versions.get('Versions')[0].get('VersionId').should.equal('v1')
-    
+
     conn.create_policy_version(
         PolicyArn="arn:aws:iam::123456789012:policy/TestListPolicyVersions",
         PolicyDocument='{"second":"policy"}')
@@ -1292,4 +1291,22 @@ def test_create_role_no_path():
     conn = boto3.client('iam', region_name='us-east-1')
     resp = conn.create_role(RoleName='my-role', AssumeRolePolicyDocument='some policy', Description='test')
     resp.get('Role').get('Arn').should.equal('arn:aws:iam::123456789012:role/my-role')
+    resp.get('Role').should_not.have.key('PermissionsBoundary')
+
+@mock_iam()
+def test_create_role_with_permissions_boundary():
+    conn = boto3.client('iam', region_name='us-east-1')
+    boundary = 'arn:aws:iam::123456789012:policy/boundary'
+    resp = conn.create_role(RoleName='my-role', AssumeRolePolicyDocument='some policy', Description='test', PermissionsBoundary=boundary)
+    expected = {
+        'PermissionsBoundaryType': 'PermissionsBoundaryPolicy',
+        'PermissionsBoundaryArn': boundary
+    }
+    resp.get('Role').get('PermissionsBoundary').should.equal(expected)
+
+    invalid_boundary_arn = 'arn:aws:iam::123456789:not_a_boundary'
+    with assert_raises(ClientError):
+        conn.create_role(RoleName='bad-boundary', AssumeRolePolicyDocument='some policy', Description='test',  PermissionsBoundary=invalid_boundary_arn)
 
+    # Ensure the PermissionsBoundary is included in role listing as well
+    conn.list_roles().get('Roles')[0].get('PermissionsBoundary').should.equal(expected)