Repository: cloud-custodian/cloud-custodian

Issue:
AWS Account - Add organization-member filter
### Describe the feature

I need to be able to check if an AWS account is part of an organization for a compliance check. I would like a filter added to the aws.account resource to do this.  The API to check if an account is part of an organization is ```response = client.describe_organization()``` and if the account is part of an organization it returns the org info. If the account is not part of an organization it will return: ```An error occurred (AWSOrganizationsNotInUseException) when calling the DescribeOrganization operation: Your account is not a member of an organization.```

### Extra information or context

The policy I want to write will look something like this:

```
- name: aws-account-organization-member
  resource: aws.account
  filters:
     - type: organization-member
       value: False
```


Discussion:

Resolving patch:
diff --git a/c7n/resources/account.py b/c7n/resources/account.py
--- a/c7n/resources/account.py
+++ b/c7n/resources/account.py
@@ -100,6 +100,71 @@ def process(self, resources, event=None):
         return results
 
 
+@filters.register('organization')
+class AccountOrganization(ValueFilter):
+    """Check organization enrollment and configuration
+
+    :example:
+
+    determine if an account is not in an organization
+
+    .. code-block:: yaml
+
+      policies:
+        - name: no-org
+          resource: account
+          filters:
+            - type: organization
+              key: Id
+              value: absent
+
+
+    :example:
+
+    determine if an account is setup for organization policies
+
+    .. code-block:: yaml
+
+       policies:
+         - name: org-policies-not-enabled
+           resource: account
+           filters:
+             - type: organization
+               key: FeatureSet
+               value: ALL
+               op: not-equal
+    """
+    schema = type_schema('organization', rinherit=ValueFilter.schema)
+    schema_alias = False
+
+    annotation_key = 'c7n:org'
+    annotate = False
+
+    permissions = ('organizations:DescribeOrganization',)
+
+    def get_org_info(self, account):
+        client = local_session(
+            self.manager.session_factory).client('organizations')
+        try:
+            org_info = client.describe_organization().get('Organization')
+        except client.exceptions.AWSOrganizationsNotInUseException:
+            org_info = {}
+        except ClientError as e:
+            self.log.warning('organization filter error accessing org info %s', e)
+            org_info = None
+        account[self.annotation_key] = org_info
+
+    def process(self, resources, event=None):
+        if self.annotation_key not in resources[0]:
+            self.get_org_info(resources[0])
+        # if we can't access org info, we've already logged, and return
+        if resources[0][self.annotation_key] is None:
+            return []
+        if super().process([resources[0][self.annotation_key]]):
+            return resources
+        return []
+
+
 @filters.register('check-macie')
 class MacieEnabled(ValueFilter):
     """Check status of macie v2 in the account.


Test patch:
diff --git a/tests/data/placebo/test_account_org_info/iam.ListAccountAliases_1.json b/tests/data/placebo/test_account_org_info/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_org_info/iam.ListAccountAliases_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "AccountAliases": [
+            "realms-root"
+        ],
+        "IsTruncated": false,
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_org_info/organizations.DescribeOrganization_1.json b/tests/data/placebo/test_account_org_info/organizations.DescribeOrganization_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_org_info/organizations.DescribeOrganization_1.json
@@ -0,0 +1,20 @@
+{
+    "status_code": 200,
+    "data": {
+        "Organization": {
+            "Id": "o-vjtynx2e3h",
+            "Arn": "arn:aws:organizations::644160558196:organization/o-vjtynx4d1h",
+            "FeatureSet": "ALL",
+            "MasterAccountArn": "arn:aws:organizations::644160558196:account/o-vjtynx4d1h/644160558196",
+            "MasterAccountId": "644160558196",
+            "MasterAccountEmail": "kapilt@gmail.com",
+            "AvailablePolicyTypes": [
+                {
+                    "Type": "SERVICE_CONTROL_POLICY",
+                    "Status": "ENABLED"
+                }
+            ]
+        },
+        "ResponseMetadata": {}
+    }
+}
diff --git a/tests/data/placebo/test_account_org_info_denied/iam.ListAccountAliases_1.json b/tests/data/placebo/test_account_org_info_denied/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_org_info_denied/iam.ListAccountAliases_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "AccountAliases": [
+            "realms-root"
+        ],
+        "IsTruncated": false,
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_org_info_denied/organizations.DescribeOrganization_1.json b/tests/data/placebo/test_account_org_info_denied/organizations.DescribeOrganization_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_org_info_denied/organizations.DescribeOrganization_1.json
@@ -0,0 +1,11 @@
+{
+    "status_code": 403,
+    "data": {
+        "Error": {
+            "Message": "not authorized",
+            "Code": "AccessDeniedException"
+        },
+        "ResponseMetadata": {},
+        "message": "not authorized"
+    }
+}
diff --git a/tests/data/placebo/test_account_org_no_org/iam.ListAccountAliases_1.json b/tests/data/placebo/test_account_org_no_org/iam.ListAccountAliases_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_org_no_org/iam.ListAccountAliases_1.json
@@ -0,0 +1,10 @@
+{
+    "status_code": 200,
+    "data": {
+        "AccountAliases": [
+            "realms-root"
+        ],
+        "IsTruncated": false,
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_account_org_no_org/organizations.DescribeOrganization_1.json b/tests/data/placebo/test_account_org_no_org/organizations.DescribeOrganization_1.json
new file mode 100644
--- /dev/null
+++ b/tests/data/placebo/test_account_org_no_org/organizations.DescribeOrganization_1.json
@@ -0,0 +1,14 @@
+{
+    "status_code": 404, 
+    "data": {
+        "ResponseMetadata": {
+            "HTTPStatusCode": 404, 
+            "HostId": "4tbXPrkIvHzDe7UTWOeiJGxuomJ8SVVvn9cSKQHVTVJdJtVeYDAkwOkd7C0K9k4WtVzvKf1R2iU=", 
+            "RequestId": "29DAAD9315A10452"
+        }, 
+        "Error": {
+            "Message": "dunno", 
+            "Code": "AWSOrganizationsNotInUseException"
+        }
+    }
+}
diff --git a/tests/test_account.py b/tests/test_account.py
--- a/tests/test_account.py
+++ b/tests/test_account.py
@@ -69,6 +69,57 @@ def test_macie_disabled(self):
         resources = p.run()
         self.assertEqual(len(resources), 1)
 
+    def test_org_no_org(self):
+        factory = self.replay_flight_data(
+            'test_account_org_no_org')
+        p = self.load_policy({
+            'name': 'org-check',
+            'resource': 'aws.account',
+            'filters': [{
+                'type': 'organization',
+                'key': 'Id',
+                'value': 'absent'
+            }]},
+            session_factory=factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+
+    def test_org_denied(self):
+        factory = self.replay_flight_data(
+            'test_account_org_info_denied')
+        p = self.load_policy({
+            'name': 'org-check',
+            'resource': 'aws.account',
+            'filters': [{
+                'type': 'organization',
+                'key': 'Id',
+                'value': 'absent'
+            }]},
+            session_factory=factory
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 0)
+
+    def test_org_info(self):
+        factory = self.replay_flight_data(
+            'test_account_org_info')
+        p = self.load_policy({
+            'name': 'org-check',
+            'resource': 'aws.account',
+            'filters': [{
+                'type': 'organization',
+                'key': 'Id',
+                'op': 'not-equal',
+                'value': 'o-xyz'
+            }]},
+            session_factory=factory
+        )
+        resources = p.run()
+
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['c7n:org']['FeatureSet'], 'ALL')
+
     def test_missing(self):
         session_factory = self.replay_flight_data(
             'test_account_missing_resource_ec2')