Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove user data from couch user model #34805

Merged
merged 11 commits into from
Jun 27, 2024
2 changes: 1 addition & 1 deletion corehq/apps/api/resources/v0_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Meta(CustomResourceMeta):

class CommCareUserResource(UserResource):
groups = fields.ListField(attribute='get_group_ids')
user_data = fields.DictField(attribute='user_data')
user_data = fields.DictField()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't the API still be trying to update the user_data attribute on the CommCareUser model? Or is that somehow otherwise accounted for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good question. That's handled here:

def _update_user_data(user, new_user_data, user_change_logger):
try:
profile_id = new_user_data.pop(PROFILE_SLUG, ...)
changed = user.get_user_data(user.domain).update(new_user_data, profile_id=profile_id)
except UserDataError as e:
raise UpdateUserException(str(e))
if user_change_logger and changed:
user_change_logger.add_changes({
'user_data': user.get_user_data(user.domain).raw
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, obj_update should manually override the update for this field.


class Meta(UserResource.Meta):
authentication = RequirePermissionAuthentication(HqPermissions.edit_commcare_users)
Expand Down
2 changes: 1 addition & 1 deletion corehq/apps/api/user_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def _update_user_data(user, new_user_data, user_change_logger):
if user_change_logger and changed:
user_change_logger.add_changes({
'user_data': user.get_user_data(user.domain).raw
})
}, skip_confirmation=True)


def _update_user_role(user, role, user_change_logger):
Expand Down
18 changes: 7 additions & 11 deletions corehq/apps/user_importer/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,15 @@ def __init__(self, upload_domain, user_domain, user, is_new_user, changed_by_use

if not is_new_user:
self.original_user_doc = self.user.to_json()
self.original_user_data = self.user.get_user_data(user_domain).raw
else:
self.original_user_doc = None
self.original_user_data = None

self.fields_changed = {}
self.change_messages = {}

self._save = False # flag to check if log needs to be saved for updates

def add_changes(self, changes):
def add_changes(self, changes, skip_confirmation=False):
"""
Add changes to user properties.
Ignored for new user since the whole user doc is logged for a new user
Expand All @@ -60,7 +58,7 @@ def add_changes(self, changes):
if self.is_new_user:
return
for name, new_value in changes.items():
if self.original_user_doc[name] != new_value:
if skip_confirmation or self.original_user_doc[name] != new_value:
self.fields_changed[name] = new_value
self._save = True

Expand Down Expand Up @@ -157,6 +155,8 @@ def update_user_data(self, data, uncategorized_data, profile_name, profiles_by_n
from corehq.apps.users.user_data import UserDataError
user_data = self.user.get_user_data(self.user_domain)
old_profile_id = user_data.profile_id
old_user_data = user_data.raw

if PROFILE_SLUG in data:
raise UserUploadError(_("You cannot set {} directly").format(PROFILE_SLUG))
if profile_name:
Expand All @@ -174,20 +174,16 @@ def update_user_data(self, data, uncategorized_data, profile_name, profiles_by_n
if user_data.profile_id and user_data.profile_id != old_profile_id:
self.logger.add_info(UserChangeMessage.profile_info(user_data.profile_id, profile_name))

if old_user_data != user_data.raw:
self.logger.add_changes({'user_data': user_data.raw}, skip_confirmation=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand we did that before too. But is logging the user data a good idea?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate? This code logs all changes made to a user for auditing purposes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is just the name of the field being user_data and in actuality it does not contain anything sensitive. But I wonder if there was a chance something that we don't want logged ends up in there. I think it would not be clear to every developer changing what goes in user_data that it will also end up in the logs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true - though logger here actually isn't a normal python logger, it's an instance of UserChangeLogger, which saves details of changes to postgres to power an auditing report.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair


def save_log(self):
# Tracking for role is done post save to have role setup correctly on save
if self.role_updated:
new_role = self.user.get_role(domain=self.user_domain)
self.logger.add_info(UserChangeMessage.role_change(new_role))

self._include_user_data_changes()
return self.logger.save()

def _include_user_data_changes(self):
new_user_data = self.user.get_user_data(self.user_domain).raw
if self.logger.original_user_data != new_user_data:
self.logger.add_changes({'user_data': new_user_data})


class CommCareUserImporter(BaseUserImporter):

Expand Down
49 changes: 0 additions & 49 deletions corehq/apps/users/management/commands/populate_sql_user_data.py

This file was deleted.

11 changes: 3 additions & 8 deletions corehq/apps/users/migrations/0057_populate_sql_user_data.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
from django.core.management import call_command
from django.db import migrations

from corehq.util.django_migrations import skip_on_fresh_install


@skip_on_fresh_install
def populate_sql_user_data(apps, schema_editor):
call_command('populate_sql_user_data')
from corehq.util.django_migrations import prompt_for_historical_migration, get_migration_name


class Migration(migrations.Migration):
Expand All @@ -16,5 +10,6 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RunPython(populate_sql_user_data, migrations.RunPython.noop)
prompt_for_historical_migration(
"users", get_migration_name(__file__), "1359e6ed5b12d929d97f5a59f0f0181a3811ccdc")
]
48 changes: 0 additions & 48 deletions corehq/apps/users/tests/test_user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
from corehq.apps.custom_data_fields.models import CustomDataFieldsProfile, Field, CustomDataFieldsDefinition
from corehq.apps.users.views.mobile.custom_data_fields import CUSTOM_USER_DATA_FIELD_TYPE
from corehq.apps.users.dbaccessors import delete_all_users
from corehq.apps.users.management.commands.populate_sql_user_data import (
get_users_without_user_data,
populate_user_data,
)
from corehq.apps.users.models import CommCareUser, WebUser
from corehq.apps.users.user_data import (
SQLUserData,
Expand Down Expand Up @@ -72,50 +68,6 @@ def test_web_users(self):
'what_domain_is_it': 'domain 2',
})

def test_migrate_get_users_without_user_data(self):
users_without_data = [
self.make_commcare_user(),
self.make_commcare_user(),
self.make_web_user(),
self.make_web_user(),
]
users_with_data = [self.make_commcare_user(), self.make_web_user()]
for user in users_with_data:
ud = user.get_user_data(self.domain)
ud['key'] = 'dummy val so this is non-empty'
ud.save()

users_to_migrate = get_users_without_user_data()
self.assertItemsEqual(
[u.username for u in users_without_data],
[u.username for u in users_to_migrate],
)

def test_migrate_commcare_user(self):
user = self.make_commcare_user()
user['user_data'] = {'favorite_color': 'purple'}
user.save()
populate_user_data(CommCareUser.get_db().get(user._id), user.get_django_user())
sql_data = SQLUserData.objects.get(domain=self.domain, user_id=user.user_id)
self.assertEqual(sql_data.data['favorite_color'], 'purple')

def test_migrate_web_user(self):
user = self.make_web_user()
# one user data dictionary, gets copied to all domains
user['user_data'] = {'favorite_color': 'purple'}
user.add_domain_membership('domain2', timezone='UTC')
user.save()
populate_user_data(WebUser.get_db().get(user._id), user.get_django_user())
for domain in [self.domain, 'domain2']:
sql_data = SQLUserData.objects.get(domain=domain, user_id=user.user_id)
self.assertEqual(sql_data.data['favorite_color'], 'purple')

def test_migrate_user_no_data(self):
user = self.make_commcare_user()
populate_user_data(CommCareUser.get_db().get(user._id), user.get_django_user())
with self.assertRaises(SQLUserData.DoesNotExist):
SQLUserData.objects.get(domain=self.domain, user_id=user.user_id)

def test_prime_user_data_caches(self):
users = [
self.make_commcare_user(),
Expand Down