Skip to content

Commit

Permalink
Merge pull request #3261 from kobotoolbox/3258-enforce-readonly-state
Browse files Browse the repository at this point in the history
Enforce readonly state at the DB router level
  • Loading branch information
JacquelineMorrissette authored Jun 3, 2021
2 parents 68e0644 + ab27300 commit 7c3ddb7
Show file tree
Hide file tree
Showing 7 changed files with 35 additions and 32 deletions.
4 changes: 2 additions & 2 deletions kobo/apps/superuser_stats/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def generate_user_report(output_filename):
from kpi.deployment_backends.kc_access.shadow_models import (
KobocatUser,
KobocatUserProfile,
ReadOnlyKobocatXForm,
KobocatXForm,
)
from hub.models import ExtraUserDetail

Expand Down Expand Up @@ -66,7 +66,7 @@ def get_row_for_user(u: KobocatUser) -> list:
else:
row_.append('')

row_.append(ReadOnlyKobocatXForm.objects.filter(user=u).count())
row_.append(KobocatXForm.objects.filter(user=u).count())

if profile:
row_.append(profile.num_of_submissions)
Expand Down
5 changes: 5 additions & 0 deletions kpi/db_routers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# coding: utf-8
from .constants import SHADOW_MODEL_APP_LABEL
from .exceptions import ReadOnlyModelError


class DefaultDatabaseRouter:
Expand All @@ -16,8 +17,12 @@ def db_for_write(self, model, **hints):
"""
Writes go to `kc` when `model` is a ShadowModel
"""
if getattr(model, 'read_only', False):
raise ReadOnlyModelError

if model._meta.app_label == SHADOW_MODEL_APP_LABEL:
return "kobocat"

return "default"

def allow_relation(self, obj1, obj2, **hints):
Expand Down
26 changes: 9 additions & 17 deletions kpi/deployment_backends/kc_access/shadow_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ def update_autofield_sequence(model):
cursor.execute(query)


class ReadOnlyModelError(ValueError):
pass


class ShadowModel(models.Model):
"""
Allows identification of writeable and read-only shadow models
Expand All @@ -68,7 +64,7 @@ class Meta:
@staticmethod
def get_content_type_for_model(model):
model_name_mapping = {
'readonlykobocatxform': ('logger', 'xform'),
'kobocatxform': ('logger', 'xform'),
'readonlykobocatinstance': ('logger', 'instance'),
'kobocatuserprofile': ('main', 'userprofile'),
'kobocatuserobjectpermission': ('guardian', 'userobjectpermission'),
Expand All @@ -81,21 +77,17 @@ def get_content_type_for_model(model):
app_label=app_label, model=model_name)


class ReadOnlyModel(ShadowModel):
class ReadOnlyShadowModel(ShadowModel):

read_only = True

class Meta(ShadowModel.Meta):
abstract = True

def save(self, *args, **kwargs):
raise ReadOnlyModelError('Cannot save read-only-model')

def delete(self, *args, **kwargs):
raise ReadOnlyModelError('Cannot delete read-only-model')
class KobocatXForm(ShadowModel):


class ReadOnlyKobocatXForm(ReadOnlyModel):

class Meta(ReadOnlyModel.Meta):
class Meta(ShadowModel.Meta):
db_table = 'logger_xform'
verbose_name = 'xform'
verbose_name_plural = 'xforms'
Expand Down Expand Up @@ -129,16 +121,16 @@ def prefixed_hash(self):
return "md5:%s" % self.hash


class ReadOnlyKobocatInstance(ReadOnlyModel):
class ReadOnlyKobocatInstance(ReadOnlyShadowModel):

class Meta(ReadOnlyModel.Meta):
class Meta(ReadOnlyShadowModel.Meta):
db_table = 'logger_instance'
verbose_name = 'instance'
verbose_name_plural = 'instances'

xml = models.TextField()
user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
xform = models.ForeignKey(ReadOnlyKobocatXForm, related_name='instances',
xform = models.ForeignKey(KobocatXForm, related_name='instances',
on_delete=models.CASCADE)
date_created = models.DateTimeField()
date_modified = models.DateTimeField()
Expand Down
10 changes: 5 additions & 5 deletions kpi/deployment_backends/kc_access/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
KobocatUserObjectPermission,
KobocatUserPermission,
KobocatUserProfile,
ReadOnlyKobocatXForm,
KobocatXForm,
)


Expand All @@ -44,17 +44,17 @@ def _trigger_kc_profile_creation(user):
@safe_kc_read
def instance_count(xform_id_string, user_id):
try:
return ReadOnlyKobocatXForm.objects.only('num_of_submissions').get(
return KobocatXForm.objects.only('num_of_submissions').get(
id_string=xform_id_string,
user_id=user_id
).num_of_submissions
except ReadOnlyKobocatXForm.DoesNotExist:
except KobocatXForm.DoesNotExist:
return 0


@safe_kc_read
def last_submission_time(xform_id_string, user_id):
return ReadOnlyKobocatXForm.objects.get(
return KobocatXForm.objects.get(
user_id=user_id, id_string=xform_id_string
).last_submission_time

Expand Down Expand Up @@ -291,7 +291,7 @@ def set_kc_anonymous_permissions_xform_flags(obj, kpi_codenames, xform_id,
flags = {flag: not value for flag, value in flags.items()}
xform_updates.update(flags)
# Write to the KC database
ReadOnlyKobocatXForm.objects.filter(pk=xform_id).update(**xform_updates)
KobocatXForm.objects.filter(pk=xform_id).update(**xform_updates)


@transaction.atomic()
Expand Down
4 changes: 2 additions & 2 deletions kpi/deployment_backends/kobocat_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from kpi.utils.log import logging
from kpi.utils.mongo_helper import MongoHelper
from .base_backend import BaseDeploymentBackend
from .kc_access.shadow_models import ReadOnlyKobocatInstance, ReadOnlyKobocatXForm
from .kc_access.shadow_models import ReadOnlyKobocatInstance, KobocatXForm
from .kc_access.utils import (
assign_applicable_kc_permissions,
instance_count,
Expand Down Expand Up @@ -192,7 +192,7 @@ def xform_id_string(self):
@property
def xform_id(self):
pk = self.asset._deployment_data.get('backend_response', {}).get('formid')
xform = ReadOnlyKobocatXForm.objects.filter(pk=pk).only(
xform = KobocatXForm.objects.filter(pk=pk).only(
'user__username', 'id_string').first()
if not (xform.user.username == self.asset.owner.username and
xform.id_string == self.xform_id_string):
Expand Down
6 changes: 6 additions & 0 deletions kpi/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ class InvalidSearchException(exceptions.APIException):
default_code = 'invalid_search'


class ReadOnlyModelError(Exception):

def __init__(self, msg='This model is read only', *args, **kwargs):
super().__init__(msg, *args, **kwargs)


class SearchQueryTooShortException(InvalidSearchException):
default_detail = _('Your query is too short')
default_code = 'query_too_short'
Expand Down
12 changes: 6 additions & 6 deletions kpi/management/commands/sync_kobocat_xforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from kpi.deployment_backends.kc_access.shadow_models import (
KobocatPermission,
KobocatUserObjectPermission,
ReadOnlyKobocatXForm,
KobocatXForm,
ShadowModel,
)
from kpi.deployment_backends.kobocat_backend import KobocatDeploymentBackend
Expand All @@ -40,7 +40,7 @@
ASSET_CT = ContentType.objects.get_for_model(Asset)
FROM_KC_ONLY_PERMISSION = Permission.objects.get(
content_type=ASSET_CT, codename=PERM_FROM_KC_ONLY)
XFORM_CT = ShadowModel.get_content_type_for_model(ReadOnlyKobocatXForm)
XFORM_CT = ShadowModel.get_content_type_for_model(KobocatXForm)
ANONYMOUS_USER = get_anonymous_user()
# Replace codenames with Permission PKs, remembering the codenames
permission_map_copy = dict(PERMISSIONS_MAP)
Expand Down Expand Up @@ -474,9 +474,9 @@ def handle(self, *args, **options):
username = options.get('username')
populate_xform_kpi_asset_uid = options.get('populate_xform_kpi_asset_uid')
users = User.objects.all()
# Do a basic query just to make sure the ReadOnlyKobocatXForm model is
# Do a basic query just to make sure the KobocatXForm model is
# loaded
if not ReadOnlyKobocatXForm.objects.exists():
if not KobocatXForm.objects.exists():
return
self._print_str('%d total users' % users.count())
# A specific user or everyone?
Expand Down Expand Up @@ -506,8 +506,8 @@ def handle(self, *args, **options):
xform_uuids_to_asset_pks[backend_response['uuid']] = \
existing_survey.pk

# ReadOnlyKobocatXForm has a foreign key on KobocatUser, not on User
xforms = ReadOnlyKobocatXForm.objects.filter(user_id=user.pk).all()
# KobocatXForm has a foreign key on KobocatUser, not on User
xforms = KobocatXForm.objects.filter(user_id=user.pk).all()
for xform in xforms:
try:
with transaction.atomic():
Expand Down

0 comments on commit 7c3ddb7

Please sign in to comment.