Repository: getmoto/moto
Pull request: Search cognito identities from iot moto backend
Search cognito identities from iot core moto backend

Problem statement:
Search cognito ids while IoT Core attach policy
Hello,

I am trying to test iot `attach_policy` with cognito identity instead of iot core certificate but it raises an exception.
Doing a research I have found that during attach_policy uses the next method in order to found the target:
```
def _get_principal(self, principal_arn):
        """
        raise ResourceNotFoundException
        """
        if ":cert/" in principal_arn:
            certs = [_ for _ in self.certificates.values() if _.arn == principal_arn]
            if len(certs) == 0:
                raise ResourceNotFoundException()
            principal = certs[0]
            return principal
        else:
            # TODO: search for cognito_ids
            pass
        raise ResourceNotFoundException()
```

I am trying to implement this TODO section but previously created identity pool and user identity are not accessible from this `_get_principal` method.

I need to know if you have thought of any way to query cognito identities from other mocked services.

Any suggestion?

Thanks!

Hints from issue discussion:
Hi @acapagarcia! You can import other service backends, and access all data stored in that service.
For performance reasons, the import should happen as late as possible.

So for this usecase, you can use:
```
def _get_principal(self, principal_arn):
        """
        raise ResourceNotFoundException
        """
        if ":cert/" in principal_arn:
            ...
        else:
            from moto.cognitoidp import cognitoidp_backends
            cognito = cognitoidp_backends["whatever-region-you're-in"]
            pools = cognito.list_user_pools()
            users = cognito.list_users()
```

I'm not too familiar with the IOT service, but let me know if that's helpful.
> Hi @acapagarcia! You can import other service backends, and access all data stored in that service.
> For performance reasons, the import should happen as late as possible.
> 
> So for this usecase, you can use:
> ```
> def _get_principal(self, principal_arn):
>         """
>         raise ResourceNotFoundException
>         """
>         if ":cert/" in principal_arn:
>             ...
>         else:
>             from moto.cognitoidp import cognitoidp_backends
>             cognito = cognitoidp_backends["whatever-region-you're-in"]
>             pools = cognito.list_user_pools()
>             users = cognito.list_users()
> ```
> 
> I'm not too familiar with the IOT service, but let me know if that's helpful.

Hi @bblommers! 
Ok. Tomorrow I will try what you say.
If everything works fine I will create a PR.


I have created new PR with changes https://github.com/spulec/moto/pull/4555

Commit message: Black on test iot file

Patch to review:
diff --git a/moto/cognitoidentity/models.py b/moto/cognitoidentity/models.py
index 291133381a4b..70a0ebea1665 100644
--- a/moto/cognitoidentity/models.py
+++ b/moto/cognitoidentity/models.py
@@ -54,6 +54,7 @@ def __init__(self, region):
         super(CognitoIdentityBackend, self).__init__()
         self.region = region
         self.identity_pools = OrderedDict()
+        self.pools_identities = {}
 
     def reset(self):
         region = self.region
@@ -105,7 +106,14 @@ def create_identity_pool(
             tags=tags,
         )
         self.identity_pools[new_identity.identity_pool_id] = new_identity
-
+        self.pools_identities.update(
+            {
+                new_identity.identity_pool_id: {
+                    "IdentityPoolId": new_identity.identity_pool_id,
+                    "Identities": [],
+                }
+            }
+        )
         response = new_identity.to_json()
         return response
 
@@ -142,8 +150,9 @@ def update_identity_pool(
         response = pool.to_json()
         return response
 
-    def get_id(self):
+    def get_id(self, identity_pool_id: str):
         identity_id = {"IdentityId": get_random_identity_id(self.region)}
+        self.pools_identities[identity_pool_id]["Identities"].append(identity_id)
         return json.dumps(identity_id)
 
     def get_credentials_for_identity(self, identity_id):
@@ -176,6 +185,10 @@ def get_open_id_token(self, identity_id):
         )
         return response
 
+    def list_identities(self, identity_pool_id, max_results=123):
+        response = json.dumps(self.pools_identities[identity_pool_id])
+        return response
+
 
 cognitoidentity_backends = {}
 for region in Session().get_available_regions("cognito-identity"):
diff --git a/moto/cognitoidentity/responses.py b/moto/cognitoidentity/responses.py
index 1910b613373d..a7b2dcf49925 100644
--- a/moto/cognitoidentity/responses.py
+++ b/moto/cognitoidentity/responses.py
@@ -53,7 +53,9 @@ def update_identity_pool(self):
         )
 
     def get_id(self):
-        return cognitoidentity_backends[self.region].get_id()
+        return cognitoidentity_backends[self.region].get_id(
+            identity_pool_id=self._get_param("IdentityPoolId"),
+        )
 
     def describe_identity_pool(self):
         return cognitoidentity_backends[self.region].describe_identity_pool(
@@ -76,3 +78,8 @@ def get_open_id_token(self):
         return cognitoidentity_backends[self.region].get_open_id_token(
             self._get_param("IdentityId") or get_random_identity_id(self.region)
         )
+
+    def list_identities(self):
+        return cognitoidentity_backends[self.region].list_identities(
+            self._get_param("IdentityPoolId") or get_random_identity_id(self.region)
+        )
diff --git a/moto/iot/models.py b/moto/iot/models.py
index 2790a11789dc..0949faea0e63 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -911,9 +911,17 @@ def _get_principal(self, principal_arn):
                 raise ResourceNotFoundException()
             principal = certs[0]
             return principal
-        else:
-            # TODO: search for cognito_ids
-            pass
+        from moto.cognitoidentity import cognitoidentity_backends
+
+        cognito = cognitoidentity_backends[self.region_name]
+        identities = []
+        for identity_pool in cognito.identity_pools:
+            pool_identities = cognito.pools_identities.get(identity_pool, None)
+            identities.extend(
+                [pi["IdentityId"] for pi in pool_identities.get("Identities", [])]
+            )
+        if principal_arn in identities:
+            return {"IdentityId": principal_arn}
         raise ResourceNotFoundException()
 
     def attach_principal_policy(self, policy_name, principal_arn):
diff --git a/tests/test_cognitoidentity/test_cognitoidentity.py b/tests/test_cognitoidentity/test_cognitoidentity.py
index 54dd26e01bf9..b30cad7c25e6 100644
--- a/tests/test_cognitoidentity/test_cognitoidentity.py
+++ b/tests/test_cognitoidentity/test_cognitoidentity.py
@@ -156,9 +156,12 @@ def test_get_random_identity_id():
 def test_get_id():
     # These two do NOT work in server mode. They just don't return the data from the model.
     conn = boto3.client("cognito-identity", "us-west-2")
+    identity_pool_data = conn.create_identity_pool(
+        IdentityPoolName="test_identity_pool", AllowUnauthenticatedIdentities=True
+    )
     result = conn.get_id(
         AccountId="someaccount",
-        IdentityPoolId="us-west-2:12345",
+        IdentityPoolId=identity_pool_data["IdentityPoolId"],
         Logins={"someurl": "12345"},
     )
     assert (
@@ -212,3 +215,21 @@ def test_get_open_id_token():
     result = conn.get_open_id_token(IdentityId="12345", Logins={"someurl": "12345"})
     assert len(result["Token"]) > 0
     assert result["IdentityId"] == "12345"
+
+
+@mock_cognitoidentity
+def test_list_identities():
+    conn = boto3.client("cognito-identity", "us-west-2")
+    identity_pool_data = conn.create_identity_pool(
+        IdentityPoolName="test_identity_pool", AllowUnauthenticatedIdentities=True
+    )
+    identity_pool_id = identity_pool_data["IdentityPoolId"]
+    identity_data = conn.get_id(
+        AccountId="someaccount",
+        IdentityPoolId=identity_pool_id,
+        Logins={"someurl": "12345"},
+    )
+    identity_id = identity_data["IdentityId"]
+    identities = conn.list_identities(IdentityPoolId=identity_pool_id, MaxResults=123)
+    assert "IdentityPoolId" in identities and "Identities" in identities
+    assert identity_id in [x["IdentityId"] for x in identities["Identities"]]
diff --git a/tests/test_cognitoidentity/test_server.py b/tests/test_cognitoidentity/test_server.py
index 4b47ecd2f3fb..5656c5b81542 100644
--- a/tests/test_cognitoidentity/test_server.py
+++ b/tests/test_cognitoidentity/test_server.py
@@ -1,4 +1,5 @@
 import json
+
 import sure  # noqa # pylint: disable=unused-import
 
 import moto.server as server
@@ -32,12 +33,21 @@ def test_get_id():
     backend = server.create_backend_app("cognito-identity")
     test_client = backend.test_client()
 
+    res = test_client.post(
+        "/",
+        data={"IdentityPoolName": "test", "AllowUnauthenticatedIdentities": True},
+        headers={
+            "X-Amz-Target": "com.amazonaws.cognito.identity.model.AWSCognitoIdentityService.CreateIdentityPool"
+        },
+    )
+
+    json_data = json.loads(res.data.decode("utf-8"))
     res = test_client.post(
         "/",
         data=json.dumps(
             {
                 "AccountId": "someaccount",
-                "IdentityPoolId": "us-west-2:12345",
+                "IdentityPoolId": json_data["IdentityPoolId"],
                 "Logins": {"someurl": "12345"},
             }
         ),
@@ -48,3 +58,48 @@ def test_get_id():
 
     json_data = json.loads(res.data.decode("utf-8"))
     assert ":" in json_data["IdentityId"]
+
+
+@mock_cognitoidentity
+def test_list_identities():
+    backend = server.create_backend_app("cognito-identity")
+    test_client = backend.test_client()
+
+    res = test_client.post(
+        "/",
+        data={"IdentityPoolName": "test", "AllowUnauthenticatedIdentities": True},
+        headers={
+            "X-Amz-Target": "com.amazonaws.cognito.identity.model.AWSCognitoIdentityService.CreateIdentityPool"
+        },
+    )
+
+    json_data = json.loads(res.data.decode("utf-8"))
+    identity_pool_id = json_data["IdentityPoolId"]
+    res = test_client.post(
+        "/",
+        data=json.dumps(
+            {
+                "AccountId": "someaccount",
+                "IdentityPoolId": identity_pool_id,
+                "Logins": {"someurl": "12345"},
+            }
+        ),
+        headers={
+            "X-Amz-Target": "com.amazonaws.cognito.identity.model.AWSCognitoIdentityService.GetId"
+        },
+    )
+
+    json_data = json.loads(res.data.decode("utf-8"))
+    identity_id = json_data["IdentityId"]
+
+    res = test_client.post(
+        "/",
+        data=json.dumps({"IdentityPoolId": identity_pool_id,}),
+        headers={
+            "X-Amz-Target": "com.amazonaws.cognito.identity.model.AWSCognitoIdentityService.ListIdentities"
+        },
+    )
+
+    json_data = json.loads(res.data.decode("utf-8"))
+    assert "IdentityPoolId" in json_data and "Identities" in json_data
+    assert identity_id in [x["IdentityId"] for x in json_data["Identities"]]
diff --git a/tests/test_iot/test_iot.py b/tests/test_iot/test_iot.py
index 84eac7f039a7..9f6888271cf9 100644
--- a/tests/test_iot/test_iot.py
+++ b/tests/test_iot/test_iot.py
@@ -2,7 +2,7 @@
 import sure  # noqa # pylint: disable=unused-import
 import boto3
 
-from moto import mock_iot
+from moto import mock_iot, mock_cognitoidentity
 from botocore.exceptions import ClientError
 import pytest
 
@@ -55,6 +55,31 @@ def test_attach_policy():
     res["policies"][0]["policyName"].should.equal("my-policy")
 
 
+@mock_iot
+@mock_cognitoidentity
+def test_attach_policy_to_identity():
+    region = "ap-northeast-1"
+
+    cognito_identity_client = boto3.client("cognito-identity", region_name=region)
+    identity_pool_name = "test_identity_pool"
+    identity_pool = cognito_identity_client.create_identity_pool(
+        IdentityPoolName=identity_pool_name, AllowUnauthenticatedIdentities=True
+    )
+    identity = cognito_identity_client.get_id(
+        AccountId="test", IdentityPoolId=identity_pool["IdentityPoolId"]
+    )
+
+    client = boto3.client("iot", region_name=region)
+    policy_name = "my-policy"
+    doc = "{}"
+    client.create_policy(policyName=policy_name, policyDocument=doc)
+    client.attach_policy(policyName=policy_name, target=identity["IdentityId"])
+
+    res = client.list_attached_policies(target=identity["IdentityId"])
+    res.should.have.key("policies").which.should.have.length_of(1)
+    res["policies"][0]["policyName"].should.equal(policy_name)
+
+
 @mock_iot
 def test_detach_policy():
     client = boto3.client("iot", region_name="ap-northeast-1")


Code review comments:
diff_hunk: @@ -911,9 +911,17 @@ def _get_principal(self, principal_arn):
                 raise ResourceNotFoundException()
             principal = certs[0]
             return principal
-        else:
-            # TODO: search for cognito_ids
-            pass
+        from moto.cognitoidentity import cognitoidentity_backends
+
+        cognito = cognitoidentity_backends[self.region_name]
+        identities = []
+        for identity_pool in cognito.identity_pools:
+            pool_identities = cognito.pools_identities.get(identity_pool, None)
+            identities.extend(
+                [pi["IdentityId"] for pi in pool_identities.get("Identities", [])]
+            )
+        if principal_arn in identities:
+            return {"IdentityId": principal_arn}
path: moto/iot/models.py
text: @user1:
```suggestion
            identities = [pi["IdentityId"] for pi in pool_identities.get("Identities", [])]
            if principal_arn in identities:
                return {"IdentityId": principal_arn}
```
If I'm not mistaken, this would be a better solution in terms of performance. No need to cycle through all identity pools if the ID exists in the first one.

@author:
I have already made the changes