Repository: getmoto/moto

Issue:
CognitoIDP: The global_sign_out action has not been implemented
self = <moto.cognitoidp.responses.CognitoIdpResponse object at 0x000002058799AA60>

    def call_action(self):
        headers = self.response_headers

        try:
            self._authenticate_and_authorize_normal_action()
        except HTTPException as http_error:
            response = http_error.description, dict(status=http_error.code)
            return self._send_response(headers, response)

        action = camelcase_to_underscores(self._get_action())
        method_names = method_names_from_class(self.__class__)
        if action in method_names:
            method = getattr(self, action)
            try:
                response = method()
            except HTTPException as http_error:
                response_headers = dict(http_error.get_headers() or [])
                response_headers["status"] = http_error.code
                response = http_error.description, response_headers

            if isinstance(response, str):
                return 200, headers, response
            else:
                return self._send_response(headers, response)

        if not action:
            return 404, headers, ""

>       raise NotImplementedError(



Discussion:
Thanks for raising this @farmankazmieshaafi - will mark it as an enhancement to add this feature!

Resolving patch:
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -578,6 +578,14 @@ def get_user_extra_data_by_client_id(self, client_id, username):
                     extra_data.update({attribute[0]["Name"]: attribute[0]["Value"]})
         return extra_data
 
+    def sign_out(self, username):
+        for token, token_tuple in list(self.refresh_tokens.items()):
+            if token_tuple is None:
+                continue
+            _, logged_in_user = token_tuple
+            if username == logged_in_user:
+                self.refresh_tokens[token] = None
+
 
 class CognitoIdpUserPoolDomain(BaseModel):
     def __init__(self, user_pool_id, domain, custom_domain_config=None):
@@ -679,6 +687,15 @@ def __init__(self, user_pool_id, group_name, description, role_arn, precedence):
         # Note that these links are bidirectional.
         self.users = set()
 
+    def update(self, description, role_arn, precedence):
+        if description is not None:
+            self.description = description
+        if role_arn is not None:
+            self.role_arn = role_arn
+        if precedence is not None:
+            self.precedence = precedence
+        self.last_modified_date = datetime.datetime.now()
+
     def to_json(self):
         return {
             "GroupName": self.group_name,
@@ -1014,6 +1031,13 @@ def delete_group(self, user_pool_id, group_name):
 
         del user_pool.groups[group_name]
 
+    def update_group(self, user_pool_id, group_name, description, role_arn, precedence):
+        group = self.get_group(user_pool_id, group_name)
+
+        group.update(description, role_arn, precedence)
+
+        return group
+
     def admin_add_user_to_group(self, user_pool_id, group_name, username):
         group = self.get_group(user_pool_id, group_name)
         user = self.admin_get_user(user_pool_id, username)
@@ -1441,12 +1465,16 @@ def admin_user_global_sign_out(self, user_pool_id, username):
         user_pool = self.describe_user_pool(user_pool_id)
         self.admin_get_user(user_pool_id, username)
 
-        for token, token_tuple in list(user_pool.refresh_tokens.items()):
-            if token_tuple is None:
-                continue
-            _, username = token_tuple
-            if username == username:
-                user_pool.refresh_tokens[token] = None
+        user_pool.sign_out(username)
+
+    def global_sign_out(self, access_token):
+        for user_pool in self.user_pools.values():
+            if access_token in user_pool.access_tokens:
+                _, username = user_pool.access_tokens[access_token]
+                user_pool.sign_out(username)
+                return
+
+        raise NotAuthorizedError(access_token)
 
     def create_resource_server(self, user_pool_id, identifier, name, scopes):
         user_pool = self.describe_user_pool(user_pool_id)
@@ -1752,6 +1780,20 @@ def add_custom_attributes(self, user_pool_id, custom_attributes):
         user_pool = self.describe_user_pool(user_pool_id)
         user_pool.add_custom_attributes(custom_attributes)
 
+    def update_user_attributes(self, access_token, attributes):
+        """
+        The parameter ClientMetadata has not yet been implemented. No CodeDeliveryDetails are returned.
+        """
+        for user_pool in self.user_pools.values():
+            if access_token in user_pool.access_tokens:
+                _, username = user_pool.access_tokens[access_token]
+                user = self.admin_get_user(user_pool.id, username)
+
+                user.update_attributes(attributes)
+                return
+
+        raise NotAuthorizedError(access_token)
+
 
 class GlobalCognitoIdpBackend(CognitoIdpBackend):
     # Some operations are unauthenticated
diff --git a/moto/cognitoidp/responses.py b/moto/cognitoidp/responses.py
--- a/moto/cognitoidp/responses.py
+++ b/moto/cognitoidp/responses.py
@@ -256,6 +256,19 @@ def delete_group(self):
         cognitoidp_backends[self.region].delete_group(user_pool_id, group_name)
         return ""
 
+    def update_group(self):
+        group_name = self._get_param("GroupName")
+        user_pool_id = self._get_param("UserPoolId")
+        description = self._get_param("Description")
+        role_arn = self._get_param("RoleArn")
+        precedence = self._get_param("Precedence")
+
+        group = cognitoidp_backends[self.region].update_group(
+            user_pool_id, group_name, description, role_arn, precedence
+        )
+
+        return json.dumps({"Group": group.to_json()})
+
     def admin_add_user_to_group(self):
         user_pool_id = self._get_param("UserPoolId")
         username = self._get_param("Username")
@@ -501,6 +514,11 @@ def admin_user_global_sign_out(self):
         )
         return ""
 
+    def global_sign_out(self):
+        access_token = self._get_param("AccessToken")
+        cognitoidp_backends[self.region].global_sign_out(access_token)
+        return ""
+
     # Resource Server
     def create_resource_server(self):
         user_pool_id = self._get_param("UserPoolId")
@@ -595,6 +613,14 @@ def add_custom_attributes(self):
         )
         return ""
 
+    def update_user_attributes(self):
+        access_token = self._get_param("AccessToken")
+        attributes = self._get_param("UserAttributes")
+        cognitoidp_backends[self.region].update_user_attributes(
+            access_token, attributes
+        )
+        return json.dumps({})
+
 
 class CognitoIdpJsonWebKeyResponse(BaseResponse):
     def __init__(self):


Test patch:
diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt
--- a/tests/terraformtests/terraform-tests.success.txt
+++ b/tests/terraformtests/terraform-tests.success.txt
@@ -24,6 +24,7 @@ cloudwatch:
   - TestAccCloudWatchLogsGroupDataSource
 cognitoidp:
   - TestAccCognitoIDPIdentityProvider
+  - TestAccCognitoIDPUserGroup_
   - TestAccCognitoIDPUserPool_
   - TestAccCognitoUser_
   - TestAccCognitoUserInGroup_
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -1316,6 +1316,44 @@ def test_create_group():
     result["Group"]["CreationDate"].should.be.a("datetime.datetime")
 
 
+@mock_cognitoidp
+def test_update_group():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"]
+    group_name = str(uuid.uuid4())
+    description = str(uuid.uuid4())
+    description2 = str(uuid.uuid4())
+    role_arn = "arn:aws:iam:::role/my-iam-role"
+    role_arn2 = "arn:aws:iam:::role/my-iam-role2"
+    precedence = random.randint(0, 100000)
+    precedence2 = random.randint(0, 100000)
+
+    conn.create_group(
+        GroupName=group_name,
+        UserPoolId=user_pool_id,
+        Description=description,
+        RoleArn=role_arn,
+        Precedence=precedence,
+    )
+
+    result = conn.update_group(
+        GroupName=group_name,
+        UserPoolId=user_pool_id,
+        Description=description2,
+        RoleArn=role_arn2,
+        Precedence=precedence2,
+    )
+
+    result["Group"]["GroupName"].should.equal(group_name)
+    result["Group"]["UserPoolId"].should.equal(user_pool_id)
+    result["Group"]["Description"].should.equal(description2)
+    result["Group"]["RoleArn"].should.equal(role_arn2)
+    result["Group"]["Precedence"].should.equal(precedence2)
+    result["Group"]["LastModifiedDate"].should.be.a("datetime.datetime")
+    result["Group"]["CreationDate"].should.be.a("datetime.datetime")
+
+
 @mock_cognitoidp
 def test_group_in_access_token():
     conn = boto3.client("cognito-idp", "us-west-2")
@@ -2990,6 +3028,36 @@ def test_admin_user_global_sign_out_unknown_user():
     err["Message"].should.equal("User does not exist.")
 
 
+@mock_cognitoidp
+def test_global_sign_out():
+    conn = boto3.client("cognito-idp", "us-west-2")
+    result = user_authentication_flow(conn)
+
+    conn.global_sign_out(AccessToken=result["access_token"])
+
+    with pytest.raises(ClientError) as ex:
+        conn.initiate_auth(
+            ClientId=result["client_id"],
+            AuthFlow="REFRESH_TOKEN",
+            AuthParameters={
+                "REFRESH_TOKEN": result["refresh_token"],
+                "SECRET_HASH": result["secret_hash"],
+            },
+        )
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("NotAuthorizedException")
+    err["Message"].should.equal("Refresh Token has been revoked")
+
+
+@mock_cognitoidp
+def test_global_sign_out_unknown_accesstoken():
+    conn = boto3.client("cognito-idp", "us-east-2")
+    with pytest.raises(ClientError) as ex:
+        conn.global_sign_out(AccessToken="n/a")
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("NotAuthorizedException")
+
+
 @mock_cognitoidp
 def test_admin_update_user_attributes():
     conn = boto3.client("cognito-idp", "us-west-2")
@@ -3146,6 +3214,41 @@ def test_admin_delete_user_attributes_non_existing_pool():
     err["Message"].should.equal(f"User pool {user_pool_id} does not exist.")
 
 
+@mock_cognitoidp
+def test_update_user_attributes():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    result = authentication_flow(conn, auth_flow="ADMIN_USER_PASSWORD_AUTH")
+    access_token = result["access_token"]
+    username = result["username"]
+    user_pool_id = result["user_pool_id"]
+
+    conn.update_user_attributes(
+        AccessToken=access_token,
+        UserAttributes=[
+            {"Name": "family_name", "Value": "Doe"},
+            {"Name": "given_name", "Value": "Jane"},
+        ],
+    )
+
+    user = conn.admin_get_user(UserPoolId=user_pool_id, Username=username)
+    attributes = user["UserAttributes"]
+
+    attributes.should.contain({"Name": "family_name", "Value": "Doe"})
+    attributes.should.contain({"Name": "given_name", "Value": "Jane"})
+
+
+@mock_cognitoidp
+def test_update_user_attributes_unknown_accesstoken():
+    conn = boto3.client("cognito-idp", "us-east-2")
+    with pytest.raises(ClientError) as ex:
+        conn.update_user_attributes(
+            AccessToken="n/a", UserAttributes=[{"Name": "a", "Value": "b"}]
+        )
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("NotAuthorizedException")
+
+
 @mock_cognitoidp
 def test_resource_server():