Repository: getmoto/moto

Issue:
Cognito - No validation that there isn't already an existing user with the same username in admin_update_user_attributes
Hi,

Sorry for the spam, just raising another issue for a potential enhancement. There is currently no validation on the `admin_update_user_attributes` function to check that the email address we are trying to update for a user isn't going to cause a conflict.

If you try to update the email address of a user to one that already exists in the user pool, a `ClientError` exception should be raised with the code `AliasExistsException`.

This piece of code should raise the exception:
```
cognito_client.admin_update_user_attributes(
      UserPoolId=user_pool_id,
      Username=user_sub,
      UserAttributes=[{"Name": "email", "Value": email_address_of_existing_user}],
)
```

Considering how bad the Cognito service is, I have a feeling it might be dependent on the configuration of the User Pool and won't always raise an exception depending on how it's configured. You might require your user pool to be configured with the following to throw this type of exception: `UsernameAttributes=["email"]`. Not 100% sure though.


Discussion:
All good @JorisLimousin - every enhancement is useful!
hi, I am interested in fixing this issue. it will be a great opportunity to fix this issue and contribute to this project if you assign me this issue . @JorisLimousin @bblommers @corasaurus-hex @olleolleolle @JackDanger 
Done @ArpanShah2k! We have some documentation on how to get started: http://docs.getmoto.org/en/latest/docs/contributing/index.html
Please let us know if you run into any issues.
Thank you sir for your kind consideration. I will go through this documentation and start working on the enhancement. I'll approach if I need help.
Respected sir,
I have read the documentation and all. but i am facing issues in
installation of moto in my laptop.

the path i went through is :
1) install python 3.10.8 will all its dependencies like pip, idle , etc.
2) install docker ( facing issues).
2) set path in cmd.
3) run commands in python and cmd to install moto. ( facing issues).



can you please help me out with this .



On Mon, Sep 12, 2022 at 2:55 PM Bert Blommers ***@***.***>
wrote:

> Done @ArpanShah2k <https://github.com/ArpanShah2k>! We have some
> documentation on how to get started:
> http://docs.getmoto.org/en/latest/docs/contributing/index.html
> Please let us know if you run into any issues.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/spulec/moto/issues/5271#issuecomment-1243459147>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/A273YZPKO2VUVT32HCMM27DV53ZJJANCNFSM5Z7NX44A>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

-- 
The information contained in this electronic communication is intended 
solely for the individual(s) or entity to which it is addressed. It may 
contain proprietary, confidential and/or legally privileged information. 
Any review, retransmission, dissemination, printing, copying or other use 
of, or taking any action in reliance on the contents of this information by 
person(s) or entities other than the intended recipient is strictly 
prohibited and may be unlawful. If you have received this communication in 
error, please notify us by responding to this email or telephone and 
immediately and permanently delete all copies of this message and any 
attachments from your system(s). The contents of this message do not 
necessarily represent the views or policies of BITS Pilani.

Don't worry about the Docker issues @ArpanShah2k - a working Docker installation is not a requirement for Cognito. (Only for other services.)

>  3) run commands in python and cmd to install moto. ( facing issues). 
>

Just to verify: you have forked Moto, and checked out your copy, before installing?

Which commands are you running, and what are the errors that you see?

I have solved

> Don't worry about the Docker issues @ArpanShah2k - a working Docker installation is not a requirement for Cognito. (Only for other services.)
> 
> > 3. run commands in python and cmd to install moto. ( facing issues).
> 
> Just to verify: you have forked Moto, and checked out your copy, before installing?
> 
> Which commands are you running, and what are the errors that you see?

I have solved this errors that i was getting while setup now.
sir i have created PR for this Issue. I request you to review it and merge it if all the test cases are cleared. 

Patch:
diff --git a/moto/cognitoidp/exceptions.py b/moto/cognitoidp/exceptions.py
--- a/moto/cognitoidp/exceptions.py
+++ b/moto/cognitoidp/exceptions.py
@@ -2,6 +2,13 @@
 from typing import Optional
 
 
+class AliasExistsException(JsonRESTError):
+    def __init__(self) -> None:
+        super().__init__(
+            "AliasExistsException", "An account with the given email already exists."
+        )
+
+
 class ResourceNotFoundError(JsonRESTError):
     def __init__(self, message: Optional[str]):
         super().__init__(error_type="ResourceNotFoundException", message=message or "")
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -11,6 +11,7 @@
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.moto_api._internal import mock_random as random
 from .exceptions import (
+    AliasExistsException,
     GroupExistsException,
     NotAuthorizedError,
     ResourceNotFoundError,
@@ -1636,6 +1637,9 @@ def admin_update_user_attributes(
     ) -> None:
         user = self.admin_get_user(user_pool_id, username)
 
+        email = self._find_attr("email", attributes)
+        self._verify_email_is_not_used(user_pool_id, email)
+
         user.update_attributes(attributes)
 
     def admin_delete_user_attributes(
@@ -2031,11 +2035,32 @@ def update_user_attributes(
                 _, username = user_pool.access_tokens[access_token]
                 user = self.admin_get_user(user_pool.id, username)
 
+                email = self._find_attr("email", attributes)
+                self._verify_email_is_not_used(user_pool.id, email)
+
                 user.update_attributes(attributes)
                 return
 
         raise NotAuthorizedError(access_token)
 
+    def _find_attr(self, name: str, attrs: List[Dict[str, str]]) -> Optional[str]:
+        return next((a["Value"] for a in attrs if a["Name"] == name), None)
+
+    def _verify_email_is_not_used(
+        self, user_pool_id: str, email: Optional[str]
+    ) -> None:
+        if not email:
+            # We're not updating emails
+            return
+        user_pool = self.describe_user_pool(user_pool_id)
+        if "email" not in user_pool.extended_config.get("UsernameAttributes", []):
+            # email is not used as a username - duplicate emails are allowed
+            return
+
+        for user in user_pool.users.values():
+            if user.attribute_lookup.get("email", "") == email:
+                raise AliasExistsException
+
 
 class RegionAgnosticBackend:
     # Some operations are unauthenticated


Test patch:
diff --git a/tests/test_cognitoidp/test_cognitoidp_exceptions.py b/tests/test_cognitoidp/test_cognitoidp_exceptions.py
--- a/tests/test_cognitoidp/test_cognitoidp_exceptions.py
+++ b/tests/test_cognitoidp/test_cognitoidp_exceptions.py
@@ -1,6 +1,8 @@
 from unittest import TestCase
 
 import boto3
+import pytest
+
 from moto import mock_cognitoidp
 from botocore.exceptions import ClientError
 
@@ -49,3 +51,47 @@ def test_authenticate_with_signed_out_user(self):
                 },
             )
         exc.exception.response["Error"]["Code"].should.equal("NotAuthorizedException")
+
+
+@mock_cognitoidp
+class TestCognitoUserPoolDuplidateEmails(TestCase):
+    def setUp(self) -> None:
+        self.client = boto3.client("cognito-idp", "us-east-1")
+
+        self.pool_id1 = self.client.create_user_pool(PoolName="test")["UserPool"]["Id"]
+        self.pool_id2 = self.client.create_user_pool(
+            PoolName="test", UsernameAttributes=["email"]
+        )["UserPool"]["Id"]
+
+        # create two users
+        for user in ["user1", "user2"]:
+            self.client.admin_create_user(
+                UserPoolId=self.pool_id1,
+                Username=user,
+                UserAttributes=[{"Name": "email", "Value": f"{user}@test.com"}],
+            )
+            self.client.admin_create_user(
+                UserPoolId=self.pool_id2,
+                Username=f"{user}@test.com",
+                UserAttributes=[{"Name": "email", "Value": f"{user}@test.com"}],
+            )
+
+    def test_use_existing_email__when_email_is_login(self):
+        with pytest.raises(ClientError) as exc:
+            self.client.admin_update_user_attributes(
+                UserPoolId=self.pool_id2,
+                Username="user1@test.com",
+                UserAttributes=[{"Name": "email", "Value": "user2@test.com"}],
+            )
+        err = exc.value.response["Error"]
+        err["Code"].should.equal("AliasExistsException")
+        err["Message"].should.equal("An account with the given email already exists.")
+
+    def test_use_existing_email__when_username_is_login(self):
+        # Because we cannot use the email as username,
+        # multiple users can have the same email address
+        self.client.admin_update_user_attributes(
+            UserPoolId=self.pool_id1,
+            Username="user1",
+            UserAttributes=[{"Name": "email", "Value": "user2@test.com"}],
+        )