Repository: cloud-custodian/cloud-custodian

Issue:
AWS:  iam-role policy not deleting roles
```
policies:
  - name: remove-role
    resource: iam-role
    filters:
      - "tag:NoDelete": absent
    actions:
      - type: delete
        force: true
```

 custodian version
```
0.9.4
```

Error:

```
2020-09-08 10:48:33,898: custodian.actions:WARNING Role:arn:aws:iam::role cannot be deleted, set force to detach policy and delete
2020-09-08 10:48:34,042: custodian.output:ERROR Error while executing policy
and delete
2020-09-08 10:48:34,042: custodian.output:ERROR Error while executing policy
Traceback (most recent call last):
 File "site-packages/c7n/policy.py", line 313, in run
   results = a.process(resources)
 File "site-packages/c7n/resources/iam.py", line 1095, in process
   raise error
 File "site-packages/c7n/resources/iam.py", line 1084, in process
   client.delete_role(RoleName=r['RoleName'])
 File "site-packages/botocore/client.py", line 316, in _api_call
   return self._make_api_call(operation_name, kwargs)
 File "site-packages/botocore/client.py", line 635, in _make_api_call
   raise error_class(parsed_response, operation_name)
botocore.errorfactory.DeleteConflictException: An error occurred (DeleteConflict) when calling the DeleteRole operation: Cannot delete entity, must delete policies first.
2020-09-08 10:48:34,044: custodian.commands:ERROR Error while executing policy remove-role, continuing
Traceback (most recent call last):
 File "site-packages/c7n/commands.py", line 272, in run
   policy()
 File "site-packages/c7n/policy.py", line 1175, in __call__
   resources = mode.run()
 File "site-packages/c7n/policy.py", line 313, in run
   results = a.process(resources)
 File "site-packages/c7n/resources/iam.py", line 1095, in process
   raise error
 File "site-packages/c7n/resources/iam.py", line 1084, in process
   client.delete_role(RoleName=r['RoleName'])
 File "site-packages/botocore/client.py", line 316, in _api_call
   return self._make_api_call(operation_name, kwargs)
 File "site-packages/botocore/client.py", line 635, in _make_api_call
   raise error_class(parsed_response, operation_name)
botocore.errorfactory.DeleteConflictException: An error occurred (DeleteConflict) when calling the DeleteRole operation: Cannot delete entity, must delete policies first.
2020-09-08 10:48:34,044: custodian.commands:ERROR The following policies had errors while executing
- remove-role
```



Discussion:
so force will detach named policies, but it looks like you have an inline policy in this case.
yes, these roles have inline policies.

Resolving patch:
diff --git a/c7n/resources/iam.py b/c7n/resources/iam.py
--- a/c7n/resources/iam.py
+++ b/c7n/resources/iam.py
@@ -1072,6 +1072,16 @@ class RoleDelete(BaseAction):
     schema = type_schema('delete', force={'type': 'boolean'})
     permissions = ('iam:DeleteRole',)
 
+    def detach_inline_policies(self, client, r):
+        policies = (self.manager.retry(
+            client.list_role_policies, RoleName=r['RoleName'],
+            ignore_err_codes=('NoSuchEntityException',)) or {}).get('PolicyNames', ())
+        for p in policies:
+            self.manager.retry(
+                client.delete_role_policy,
+                RoleName=r['RoleName'], PolicyName=p,
+                ignore_err_codes=('NoSuchEntityException',))
+
     def process(self, resources):
         client = local_session(self.manager.session_factory).client('iam')
         error = None
@@ -1079,7 +1089,10 @@ def process(self, resources):
             policy_setter = self.manager.action_registry['set-policy'](
                 {'state': 'detached', 'arn': '*'}, self.manager)
             policy_setter.process(resources)
+
         for r in resources:
+            if self.data.get('force', False):
+                self.detach_inline_policies(client, r)
             try:
                 client.delete_role(RoleName=r['RoleName'])
             except client.exceptions.DeleteConflictException as e:
@@ -1087,9 +1100,8 @@ def process(self, resources):
                     "Role:%s cannot be deleted, set force to detach policy and delete"
                     % r['Arn'])
                 error = e
-            except client.exceptions.NoSuchEntityException:
-                continue
-            except client.exceptions.UnmodifiableEntityException:
+            except (client.exceptions.NoSuchEntityException,
+                    client.exceptions.UnmodifiableEntityException):
                 continue
         if error:
             raise error


Test patch:
diff --git a/tests/data/placebo/test_force_delete_role/iam.ListRolePolicies_1.json b/tests/data/placebo/test_force_delete_role/iam.ListRolePolicies_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_force_delete_role/iam.ListRolePolicies_1.json
@@ -0,0 +1,8 @@
+{
+    "status_code": 200,
+    "data": {
+        "PolicyNames": [],
+        "IsTruncated": false,
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_iam_role_delete/iam.DeleteRolePolicy_1.json b/tests/data/placebo/test_iam_role_delete/iam.DeleteRolePolicy_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.DeleteRolePolicy_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_iam_role_delete/iam.DeleteRole_1.json b/tests/data/placebo/test_iam_role_delete/iam.DeleteRole_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.DeleteRole_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_iam_role_delete/iam.DetachRolePolicy_1.json b/tests/data/placebo/test_iam_role_delete/iam.DetachRolePolicy_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.DetachRolePolicy_1.json
@@ -0,0 +1,6 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_iam_role_delete/iam.GetRole_1.json b/tests/data/placebo/test_iam_role_delete/iam.GetRole_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.GetRole_1.json
@@ -0,0 +1,31 @@
+{
+    "status_code": 200,
+    "data": {
+        "Role": {
+            "Path": "/",
+            "RoleName": "test_role",
+            "RoleId": "AROAZL6XWCR2LYNIQ2V3Y",
+            "Arn": "arn:aws:iam::644160558196:role/test_role",
+            "CreateDate": {
+                "__class__": "datetime",
+                "year": 2020,
+                "month": 9,
+                "day": 10,
+                "hour": 17,
+                "minute": 11,
+                "second": 22,
+                "microsecond": 0
+            },
+            "AssumeRolePolicyDocument": "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%5B%22ec2.amazonaws.com%22%2C%22lambda.amazonaws.com%22%5D%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D",
+            "MaxSessionDuration": 3600,
+            "Tags": [
+                {
+                    "Key": "tag-key",
+                    "Value": "tag-value"
+                }
+            ],
+            "RoleLastUsed": {}
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_iam_role_delete/iam.GetRole_2.json b/tests/data/placebo/test_iam_role_delete/iam.GetRole_2.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.GetRole_2.json
@@ -0,0 +1,31 @@
+{
+    "status_code": 200,
+    "data": {
+        "Role": {
+            "Path": "/",
+            "RoleName": "test_role",
+            "RoleId": "AROAZL6XWCR2LYNIQ2V3Y",
+            "Arn": "arn:aws:iam::644160558196:role/test_role",
+            "CreateDate": {
+                "__class__": "datetime",
+                "year": 2020,
+                "month": 9,
+                "day": 10,
+                "hour": 17,
+                "minute": 11,
+                "second": 22,
+                "microsecond": 0
+            },
+            "AssumeRolePolicyDocument": "%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%5B%22ec2.amazonaws.com%22%2C%22lambda.amazonaws.com%22%5D%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D",
+            "MaxSessionDuration": 3600,
+            "Tags": [
+                {
+                    "Key": "tag-key",
+                    "Value": "tag-value"
+                }
+            ],
+            "RoleLastUsed": {}
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_iam_role_delete/iam.GetRole_3.json b/tests/data/placebo/test_iam_role_delete/iam.GetRole_3.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.GetRole_3.json
@@ -0,0 +1,11 @@
+{
+    "status_code": 404,
+    "data": {
+        "Error": {
+            "Type": "Sender",
+            "Code": "NoSuchEntity",
+            "Message": "The role with name test_role cannot be found."
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_iam_role_delete/iam.ListAttachedRolePolicies_1.json b/tests/data/placebo/test_iam_role_delete/iam.ListAttachedRolePolicies_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.ListAttachedRolePolicies_1.json
@@ -0,0 +1,13 @@
+{
+    "status_code": 200,
+    "data": {
+        "AttachedPolicies": [
+            {
+                "PolicyName": "test-policy",
+                "PolicyArn": "arn:aws:iam::644160558196:policy/test-policy"
+            }
+        ],
+        "IsTruncated": false,
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_iam_role_delete/iam.ListRolePolicies_1.json b/tests/data/placebo/test_iam_role_delete/iam.ListRolePolicies_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_iam_role_delete/iam.ListRolePolicies_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "PolicyNames": [
+            "test_policy"
+        ],
+        "IsTruncated": false,
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/terraform/iam_role_delete/lambda.tf b/tests/terraform/iam_role_delete/lambda.tf
new file mode 100644
--- /dev/null
+++ b/tests/terraform/iam_role_delete/lambda.tf
@@ -0,0 +1,19 @@
+resource "aws_lambda_function" "test_lambda" {
+  filename      = "payload.zip"
+  function_name = "test_function_role"
+  role          = aws_iam_role.test_role.arn
+  handler       = "main.handler"
+
+  # The filebase64sha256() function is available in Terraform 0.11.12 and later
+  # For Terraform 0.11.11 and earlier, use the base64sha256() function and the file() function:
+  # source_code_hash = "${base64sha256(file("lambda_function_payload.zip"))}"
+  source_code_hash = filebase64sha256("payload.zip")
+
+  runtime = "python3.8"
+
+  environment {
+    variables = {
+      foo = "bar"
+    }
+  }
+}
diff --git a/tests/terraform/iam_role_delete/payload.zip b/tests/terraform/iam_role_delete/payload.zip
new file mode 100644
Binary files /dev/null and b/tests/terraform/iam_role_delete/payload.zip differ
diff --git a/tests/terraform/iam_role_delete/role.tf b/tests/terraform/iam_role_delete/role.tf
new file mode 100644
--- /dev/null
+++ b/tests/terraform/iam_role_delete/role.tf
@@ -0,0 +1,79 @@
+resource "aws_iam_role" "test_role" {
+  name = "test_role"
+
+  assume_role_policy = <<EOF
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Action": "sts:AssumeRole",
+      "Principal": {
+        "Service": ["ec2.amazonaws.com", "lambda.amazonaws.com"]
+      },
+      "Effect": "Allow",
+      "Sid": ""
+    }
+  ]
+}
+EOF
+
+  tags = {
+    tag-key = "tag-value"
+  }
+}
+
+
+//resource "aws_iam_instance_profile" "test_profile" {
+//  name = "test_profile"
+//  role = aws_iam_role.test_role.name
+//}
+
+
+// inline policy
+
+resource "aws_iam_role_policy" "test_policy" {
+  name = "test_policy"
+  role = aws_iam_role.test_role.id
+
+  policy = <<-EOF
+  {
+    "Version": "2012-10-17",
+    "Statement": [
+      {
+        "Action": [
+          "ec2:Describe*"
+        ],
+        "Effect": "Allow",
+        "Resource": "*"
+      }
+    ]
+  }
+  EOF
+}
+
+
+// attached policy
+resource "aws_iam_policy" "policy" {
+  name        = "test-policy"
+  description = "A test policy"
+
+  policy = <<EOF
+{
+  "Version": "2012-10-17",
+  "Statement": [
+    {
+      "Action": [
+        "ec2:Describe*"
+      ],
+      "Effect": "Allow",
+      "Resource": "*"
+    }
+  ]
+}
+EOF
+}
+
+resource "aws_iam_role_policy_attachment" "test-attach" {
+  role       = aws_iam_role.test_role.name
+  policy_arn = aws_iam_policy.policy.arn
+}
diff --git a/tests/terraform/iam_role_delete/tf_resources.json b/tests/terraform/iam_role_delete/tf_resources.json
new file mode 100644
--- /dev/null
+++ b/tests/terraform/iam_role_delete/tf_resources.json
@@ -0,0 +1,96 @@
+{
+    "pytest-terraform": 1,
+    "outputs": {},
+    "resources": {
+        "aws_iam_policy": {
+            "policy": {
+                "arn": "arn:aws:iam::644160558196:policy/test-policy",
+                "description": "A test policy",
+                "id": "arn:aws:iam::644160558196:policy/test-policy",
+                "name": "test-policy",
+                "name_prefix": null,
+                "path": "/",
+                "policy": "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Action\": [\n        \"ec2:Describe*\"\n      ],\n      \"Effect\": \"Allow\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n"
+            }
+        },
+        "aws_iam_role": {
+            "test_role": {
+                "arn": "arn:aws:iam::644160558196:role/test_role",
+                "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\",\"lambda.amazonaws.com\"]},\"Action\":\"sts:AssumeRole\"}]}",
+                "create_date": "2020-09-10T17:11:22Z",
+                "description": "",
+                "force_detach_policies": false,
+                "id": "test_role",
+                "max_session_duration": 3600,
+                "name": "test_role",
+                "name_prefix": null,
+                "path": "/",
+                "permissions_boundary": null,
+                "tags": {
+                    "tag-key": "tag-value"
+                },
+                "unique_id": "AROAZL6XWCR2LYNIQ2V3Y"
+            }
+        },
+        "aws_iam_role_policy": {
+            "test_policy": {
+                "id": "test_role:test_policy",
+                "name": "test_policy",
+                "name_prefix": null,
+                "policy": "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Action\": [\n        \"ec2:Describe*\"\n      ],\n      \"Effect\": \"Allow\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n",
+                "role": "test_role"
+            }
+        },
+        "aws_iam_role_policy_attachment": {
+            "test-attach": {
+                "id": "test_role-644160558196",
+                "policy_arn": "arn:aws:iam::644160558196:policy/test-policy",
+                "role": "test_role"
+            }
+        },
+        "aws_lambda_function": {
+            "test_lambda": {
+                "arn": "arn:aws:lambda:us-east-1:644160558196:function:test_function_role",
+                "dead_letter_config": [],
+                "description": "",
+                "environment": [
+                    {
+                        "variables": {
+                            "foo": "bar"
+                        }
+                    }
+                ],
+                "file_system_config": [],
+                "filename": "payload.zip",
+                "function_name": "test_function_role",
+                "handler": "main.handler",
+                "id": "test_function_role",
+                "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:644160558196:function:test_function_role/invocations",
+                "kms_key_arn": "",
+                "last_modified": "2020-09-10T17:11:30.820+0000",
+                "layers": null,
+                "memory_size": 128,
+                "publish": false,
+                "qualified_arn": "arn:aws:lambda:us-east-1:644160558196:function:test_function_role:$LATEST",
+                "reserved_concurrent_executions": -1,
+                "role": "arn:aws:iam::644160558196:role/test_role",
+                "runtime": "python3.8",
+                "s3_bucket": null,
+                "s3_key": null,
+                "s3_object_version": null,
+                "source_code_hash": "s6uSwd2/b8HQOfHGDIFWDjJHusxPkXBlqgfzJPbZR6g=",
+                "source_code_size": 216,
+                "tags": null,
+                "timeout": 3,
+                "timeouts": null,
+                "tracing_config": [
+                    {
+                        "mode": "PassThrough"
+                    }
+                ],
+                "version": "$LATEST",
+                "vpc_config": []
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_iam.py b/tests/test_iam.py
--- a/tests/test_iam.py
+++ b/tests/test_iam.py
@@ -368,6 +368,36 @@ def test_iam_mfa_filter(self):
         self.assertEqual(len(resources), 2)
 
 
+@terraform('iam_role_delete', teardown=terraform.TEARDOWN_IGNORE)
+def test_iam_role_delete(test, iam_role_delete):
+    session_factory = test.replay_flight_data('test_iam_role_delete')
+    client = session_factory().client('iam')
+    pdata = {
+        'name': 'group-delete',
+        'resource': 'iam-role',
+        'mode': {
+            'type': 'cloudtrail',
+            'events': [{
+                'source': 'source',
+                'event': 'event',
+                'ids': "RoleNames"}]
+        },
+        'actions': [{'type': 'delete', 'force': True}]
+    }
+
+    event = {'detail': {
+        'eventName': 'event', 'eventSource': 'source',
+        'RoleNames': [iam_role_delete['aws_iam_role.test_role.name']]}}
+    if test.recording:
+        time.sleep(3)
+    p = test.load_policy(pdata, session_factory=session_factory)
+    resources = p.push(event)
+    assert len(resources) == 1
+
+    with pytest.raises(client.exceptions.NoSuchEntityException):
+        client.get_role(RoleName=iam_role_delete['aws_iam_role.test_role.name'])
+
+
 class IamRoleTest(BaseTest):
 
     def test_iam_role_post(self):