Repository: oppia/oppia

Issue:
[BUG]: Deletion policy is implemented incorrectly for UserSubscriptionsModel and UserSubscribersModel
### Describe the bug

The UserSubscriptionsModel and UserSubscribersModel `apply_deletion_policy()` methods are implemented incorrectly. For the former: when a user is deleted, then deleting the model corresponding to the deleted user, the method also finds all models which subscribe to explorations created by a deleted user, and deletes those latter models. For the latter: when a user who has subscribed to an existing user is deleted, the entire model for that latter user is also deleted.

This is too drastic -- we should just remove those creators/subscribers from the corresponding models, rather than deleting the entire model.

Tests should also be written to cover this case.

### Steps To Reproduce

On a local development server:

1. Log in as user A, create an exploration and publish it.
2. Log in as user B, create an exploration and publish it. 
3. Log in as user C and subscribe to users A and B.
4. Log in as user A and delete your account.
5. Log in as user C.
6. Log in as user D and subscribe to user B.
7. Log in as user C and delete your account.

### Expected Behavior

After step 5, you should still see the subscription to user B. The subscription for user A should not show up. In the datastore, the UserSubscriptionsModel for user C should still exist and show a subscription for user B.

After step 7, in the datastore, the UserSubscribersModel for user B should still exist and show a subscription from user D.

(Previously: the current logic would have deleted the UserSubscriptionsModel for user C and the UserSubscribersModel for user B.)

### Screenshots/Videos

_No response_

### What device are you using?

Desktop

### Operating System

Linux

### What browsers are you seeing the problem on?

_No response_

### Browser version

_No response_

### Additional context

This issue was discovered during an audit of the deletion policies. In this case, we actually delete more data than necessary but this leads to a suboptimal user experience for other users.
[BUG]: Deletion policy is implemented incorrectly for UserSubscriptionsModel and UserSubscribersModel
### Describe the bug

The UserSubscriptionsModel and UserSubscribersModel `apply_deletion_policy()` methods are implemented incorrectly. For the former: when a user is deleted, then deleting the model corresponding to the deleted user, the method also finds all models which subscribe to explorations created by a deleted user, and deletes those latter models. For the latter: when a user who has subscribed to an existing user is deleted, the entire model for that latter user is also deleted.

This is too drastic -- we should just remove those creators/subscribers from the corresponding models, rather than deleting the entire model.

Tests should also be written to cover this case.

### Steps To Reproduce

On a local development server:

1. Log in as user A, create an exploration and publish it.
2. Log in as user B, create an exploration and publish it. 
3. Log in as user C and subscribe to users A and B.
4. Log in as user A and delete your account.
5. Log in as user C.
6. Log in as user D and subscribe to user B.
7. Log in as user C and delete your account.

### Expected Behavior

After step 5, you should still see the subscription to user B. The subscription for user A should not show up. In the datastore, the UserSubscriptionsModel for user C should still exist and show a subscription for user B.

After step 7, in the datastore, the UserSubscribersModel for user B should still exist and show a subscription from user D.

(Previously: the current logic would have deleted the UserSubscriptionsModel for user C and the UserSubscribersModel for user B.)

### Screenshots/Videos

_No response_

### What device are you using?

Desktop

### Operating System

Linux

### What browsers are you seeing the problem on?

_No response_

### Browser version

_No response_

### Additional context

This issue was discovered during an audit of the deletion policies. In this case, we actually delete more data than necessary but this leads to a suboptimal user experience for other users.


Discussion:
@seanlip This makes sense, I don't think we need to do any job fixing this stuff in a past, I will just change the apply functions to make sure that they work correctly for future wipeouts.
@seanlip This makes sense, I don't think we need to do any job fixing this stuff in a past, I will just change the apply functions to make sure that they work correctly for future wipeouts.

Resolving patch:
diff --git a/core/storage/user/gae_models.py b/core/storage/user/gae_models.py
--- a/core/storage/user/gae_models.py
+++ b/core/storage/user/gae_models.py
@@ -1130,8 +1130,21 @@ def apply_deletion_policy(cls, user_id: str) -> None:
         Args:
             user_id: str. The ID of the user whose data should be deleted.
         """
-        keys = cls.query(cls.creator_ids == user_id).fetch(keys_only=True)
-        datastore_services.delete_multi(keys)
+        user_subscriptions_models: List[UserSubscriptionsModel] = list(
+            cls.query(
+                cls.creator_ids == user_id
+            ).fetch()
+        )
+
+        for user_subscribers_model in user_subscriptions_models:
+            user_subscribers_model.creator_ids.remove(user_id)
+
+        # Delete the references to this user from other user subscriptions
+        # models.
+        cls.update_timestamps_multi(user_subscriptions_models)
+        cls.put_multi(user_subscriptions_models)
+
+        # Delete the model for the user.
         cls.delete_by_id(user_id)
 
     @classmethod
@@ -1147,8 +1160,10 @@ def has_reference_to_user_id(cls, user_id: str) -> bool:
         """
         return (
             cls.query(
-                cls.creator_ids == user_id).get(keys_only=True) is not None or
-            cls.get_by_id(user_id) is not None)
+                cls.creator_ids == user_id
+            ).get(keys_only=True) is not None or
+            cls.get_by_id(user_id) is not None
+        )
 
     @staticmethod
     def export_data(user_id: str) -> Dict[str, Union[List[str], float, None]]:
@@ -1217,8 +1232,18 @@ def apply_deletion_policy(cls, user_id: str) -> None:
         Args:
             user_id: str. The ID of the user whose data should be deleted.
         """
-        keys = cls.query(cls.subscriber_ids == user_id).fetch(keys_only=True)
-        datastore_services.delete_multi(keys)
+        user_subscribers_models: List[UserSubscribersModel] = list(cls.query(
+            cls.subscriber_ids == user_id
+        ).fetch())
+
+        for user_subscribers_model in user_subscribers_models:
+            user_subscribers_model.subscriber_ids.remove(user_id)
+
+        # Delete the references to this user from other user subscribers models.
+        cls.update_timestamps_multi(user_subscribers_models)
+        cls.put_multi(user_subscribers_models)
+
+        # Delete the model for the user.
         cls.delete_by_id(user_id)
 
     @classmethod


Test patch:
diff --git a/core/storage/user/gae_models_test.py b/core/storage/user/gae_models_test.py
--- a/core/storage/user/gae_models_test.py
+++ b/core/storage/user/gae_models_test.py
@@ -1178,6 +1178,7 @@ def setUp(self) -> None:
                 username='username' + creator_id,
                 email=creator_id + '@example.com'
             ).put()
+            user_models.UserSubscriptionsModel(id=creator_id).put()
 
         user_models.UserSubscriptionsModel(
             id=self.USER_ID_2,
@@ -1261,10 +1262,18 @@ def test_get_field_names_for_takeout(self) -> None:
             }
         )
 
-    def test_apply_deletion_policy(self) -> None:
+    def test_apply_deletion_policy_deletes_model_for_user(self) -> None:
         user_models.UserSubscriptionsModel.apply_deletion_policy(self.USER_ID_1)
         self.assertIsNone(
             user_models.UserSubscriptionsModel.get_by_id(self.USER_ID_1))
+
+    def test_apply_deletion_policy_deletes_user_from_creator_ids(self) -> None:
+        user_models.UserSubscriptionsModel.apply_deletion_policy(self.USER_ID_5)
+        user_subscriptions_model = (
+            user_models.UserSubscriptionsModel.get_by_id(self.USER_ID_2))
+        self.assertNotIn(self.USER_ID_5, user_subscriptions_model.creator_ids)
+
+    def test_apply_deletion_policy_for_non_existing_user_passes(self) -> None:
         # Test that calling apply_deletion_policy with no existing model
         # doesn't fail.
         user_models.UserSubscriptionsModel.apply_deletion_policy(
@@ -1358,16 +1367,25 @@ def setUp(self) -> None:
         user_models.UserSubscribersModel(
             id=self.USER_ID_1, subscriber_ids=[self.USER_ID_3]).put()
         user_models.UserSubscribersModel(id=self.USER_ID_2, deleted=True).put()
+        user_models.UserSubscribersModel(id=self.USER_ID_3).put()
 
     def test_get_deletion_policy(self) -> None:
         self.assertEqual(
             user_models.UserSubscribersModel.get_deletion_policy(),
             base_models.DELETION_POLICY.DELETE)
 
-    def test_apply_deletion_policy(self) -> None:
+    def test_apply_deletion_policy_deletes_model_for_user(self) -> None:
         user_models.UserSubscribersModel.apply_deletion_policy(self.USER_ID_1)
         self.assertIsNone(
             user_models.UserSubscribersModel.get_by_id(self.USER_ID_1))
+
+    def test_apply_deletion_policy_deletes_user_from_creator_ids(self) -> None:
+        user_models.UserSubscribersModel.apply_deletion_policy(self.USER_ID_3)
+        user_subscribers_model = (
+            user_models.UserSubscribersModel.get_by_id(self.USER_ID_1))
+        self.assertNotIn(self.USER_ID_3, user_subscribers_model.subscriber_ids)
+
+    def test_apply_deletion_policy_for_non_existing_user_passes(self) -> None:
         # Test that calling apply_deletion_policy with no existing model
         # doesn't fail.
         user_models.UserSubscribersModel.apply_deletion_policy(