Repository: getmoto/moto

Issue:
Feature request: Implement IAM back end part of update_assume_role_policy to validate policies
Hello,
The current implementation of update_assume_role_policy for IAM can validate the policy passed in.
I would like to validate this data, so that invalid policies will receive an exception



Discussion:
Thanks for raising this @mmerrill3. What kind of validation did you have in mind? Do you have an example of an invalid policy that should fail?
Hi @bblommers , I was thinking to reuse the policy validator function that is used already in the IAM package, the IAMPolicyDocumentValidator.  That should be fine.  It does a pretty decent job of validating the sections of the IAM policy.  I'm not looking to do a deep inspection of all the values.  For instance, if the account really exists in the principal or the resources.  I think the general regular expressions used are enough.  

I have a feature branch with the updates and the unit test changes ready, I'll issue a PR this week for this.  You can assign to me.
/assign mmerrill3

Patch:
diff --git a/moto/iam/models.py b/moto/iam/models.py
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -21,7 +21,10 @@
     iso_8601_datetime_with_milliseconds,
     BackendDict,
 )
-from moto.iam.policy_validation import IAMPolicyDocumentValidator
+from moto.iam.policy_validation import (
+    IAMPolicyDocumentValidator,
+    IAMTrustPolicyDocumentValidator,
+)
 from moto.utilities.utils import md5_hash
 
 from .aws_managed_policies import aws_managed_policies_data
@@ -1848,6 +1851,12 @@ def delete_role(self, role_name):
     def get_roles(self):
         return self.roles.values()
 
+    def update_assume_role_policy(self, role_name, policy_document):
+        role = self.get_role(role_name)
+        iam_policy_document_validator = IAMTrustPolicyDocumentValidator(policy_document)
+        iam_policy_document_validator.validate()
+        role.assume_role_policy_document = policy_document
+
     def put_role_policy(self, role_name, policy_name, policy_json):
         role = self.get_role(role_name)
 
diff --git a/moto/iam/policy_validation.py b/moto/iam/policy_validation.py
--- a/moto/iam/policy_validation.py
+++ b/moto/iam/policy_validation.py
@@ -83,7 +83,7 @@
 }
 
 
-class IAMPolicyDocumentValidator:
+class BaseIAMPolicyValidator:
     def __init__(self, policy_document):
         self._policy_document = policy_document
         self._policy_json = {}
@@ -117,10 +117,6 @@ def validate(self):
             self._validate_action_like_exist()
         except Exception:
             raise MalformedPolicyDocument("Policy statement must contain actions.")
-        try:
-            self._validate_resource_exist()
-        except Exception:
-            raise MalformedPolicyDocument("Policy statement must contain resources.")
 
         if self._resource_error != "":
             raise MalformedPolicyDocument(self._resource_error)
@@ -521,3 +517,55 @@ def _validate_iso_8601_datetime(datetime):
             if seconds_with_decimal_fraction_partition[1] == ".":
                 decimal_seconds = seconds_with_decimal_fraction_partition[2]
                 assert 0 <= int(decimal_seconds) <= 999999999
+
+
+class IAMPolicyDocumentValidator(BaseIAMPolicyValidator):
+    def __init__(self, policy_document):
+        super().__init__(policy_document)
+
+    def validate(self):
+        super().validate()
+        try:
+            self._validate_resource_exist()
+        except Exception:
+            raise MalformedPolicyDocument("Policy statement must contain resources.")
+
+
+class IAMTrustPolicyDocumentValidator(BaseIAMPolicyValidator):
+    def __init__(self, policy_document):
+        super().__init__(policy_document)
+
+    def validate(self):
+        super().validate()
+        try:
+            for statement in self._statements:
+                if isinstance(statement["Action"], str):
+                    IAMTrustPolicyDocumentValidator._validate_trust_policy_action(
+                        statement["Action"]
+                    )
+                else:
+                    for action in statement["Action"]:
+                        IAMTrustPolicyDocumentValidator._validate_trust_policy_action(
+                            action
+                        )
+        except Exception:
+            raise MalformedPolicyDocument(
+                "Trust Policy statement actions can only be sts:AssumeRole, "
+                "sts:AssumeRoleWithSAML,  and sts:AssumeRoleWithWebIdentity"
+            )
+        try:
+            self._validate_resource_not_exist()
+        except Exception:
+            raise MalformedPolicyDocument("Has prohibited field Resource.")
+
+    def _validate_resource_not_exist(self):
+        for statement in self._statements:
+            assert "Resource" not in statement and "NotResource" not in statement
+
+    @staticmethod
+    def _validate_trust_policy_action(action):
+        assert action in (
+            "sts:AssumeRole",
+            "sts:AssumeRoleWithSAML",
+            "sts:AssumeRoleWithWebIdentity",
+        )
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -255,8 +255,8 @@ def get_role_policy(self):
 
     def update_assume_role_policy(self):
         role_name = self._get_param("RoleName")
-        role = self.backend.get_role(role_name)
-        role.assume_role_policy_document = self._get_param("PolicyDocument")
+        policy_document = self._get_param("PolicyDocument")
+        self.backend.update_assume_role_policy(role_name, policy_document)
         template = self.response_template(GENERIC_EMPTY_TEMPLATE)
         return template.render(name="UpdateAssumeRolePolicy")
 


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
@@ -377,14 +377,107 @@ def test_get_role_policy():
 
 
 @mock_iam
-def test_update_assume_role_policy():
+def test_update_assume_role_invalid_policy():
     conn = boto3.client("iam", region_name="us-east-1")
     conn.create_role(
         RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="my-path"
     )
-    conn.update_assume_role_policy(RoleName="my-role", PolicyDocument="new policy")
+    with pytest.raises(ClientError) as ex:
+        conn.update_assume_role_policy(RoleName="my-role", PolicyDocument="new policy")
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("MalformedPolicyDocument")
+    err["Message"].should.contain("Syntax errors in policy.")
+
+
+@mock_iam
+def test_update_assume_role_valid_policy():
+    conn = boto3.client("iam", region_name="us-east-1")
+    conn.create_role(
+        RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="my-path"
+    )
+    policy_document = """
+    {
+        "Version": "2012-10-17",
+        "Statement": [
+            {
+                "Effect": "Allow",
+                "Principal": {
+                    "Service": ["ec2.amazonaws.com"]
+                },
+                "Action": ["sts:AssumeRole"]
+            }
+        ]
+    }
+"""
+    conn.update_assume_role_policy(RoleName="my-role", PolicyDocument=policy_document)
     role = conn.get_role(RoleName="my-role")["Role"]
-    role["AssumeRolePolicyDocument"].should.equal("new policy")
+    role["AssumeRolePolicyDocument"]["Statement"][0]["Action"][0].should.equal(
+        "sts:AssumeRole"
+    )
+
+
+@mock_iam
+def test_update_assume_role_invalid_policy_bad_action():
+    conn = boto3.client("iam", region_name="us-east-1")
+    conn.create_role(
+        RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="my-path"
+    )
+    policy_document = """
+    {
+        "Version": "2012-10-17",
+        "Statement": [
+            {
+                "Effect": "Allow",
+                "Principal": {
+                    "Service": ["ec2.amazonaws.com"]
+                },
+                "Action": ["sts:BadAssumeRole"]
+            }
+        ]
+    }
+"""
+
+    with pytest.raises(ClientError) as ex:
+        conn.update_assume_role_policy(
+            RoleName="my-role", PolicyDocument=policy_document
+        )
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("MalformedPolicyDocument")
+    err["Message"].should.contain(
+        "Trust Policy statement actions can only be sts:AssumeRole, "
+        "sts:AssumeRoleWithSAML,  and sts:AssumeRoleWithWebIdentity"
+    )
+
+
+@mock_iam
+def test_update_assume_role_invalid_policy_with_resource():
+    conn = boto3.client("iam", region_name="us-east-1")
+    conn.create_role(
+        RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="my-path"
+    )
+    policy_document = """
+    {
+        "Version": "2012-10-17",
+        "Statement": [
+            {
+                "Effect": "Allow",
+                "Principal": {
+                    "Service": ["ec2.amazonaws.com"]
+                },
+                "Action": ["sts:AssumeRole"],
+                "Resource" : "arn:aws:s3:::example_bucket"
+            }
+        ]
+    }
+    """
+
+    with pytest.raises(ClientError) as ex:
+        conn.update_assume_role_policy(
+            RoleName="my-role", PolicyDocument=policy_document
+        )
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("MalformedPolicyDocument")
+    err["Message"].should.contain("Has prohibited field Resource.")
 
 
 @mock_iam
diff --git a/tests/test_iam/test_iam_policies.py b/tests/test_iam/test_iam_policies.py
--- a/tests/test_iam/test_iam_policies.py
+++ b/tests/test_iam/test_iam_policies.py
@@ -317,7 +317,7 @@
             "Version": "2012-10-17",
             "Statement": {"Effect": "Allow", "Action": "invalid"},
         },
-        "error_message": "Policy statement must contain resources.",
+        "error_message": "Actions/Conditions must be prefaced by a vendor, e.g., iam, sdb, ec2, etc.",
     },
     {
         "document": {