-
-
Save thismatters/53787f2d021fa5a1df640cd7b98d1185 to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*- | |
from __future__ import unicode_literals | |
from django.db import migrations, models | |
from django_cryptography.fields import encrypt | |
app_with_model = 'account' | |
model_with_column = 'User' | |
column_to_encrypt = 'email_address' | |
column_field_class = models.CharField | |
column_attrs = {'max_length': 150} | |
column_null_status = False | |
temporary_column = f'temp_{column_to_encrypt}' | |
def replicate_to_temporary(apps, schema_editor): | |
Model = apps.get_model(app_with_model, model_with_column) | |
for row in Model.objects.all(): | |
setattr(row, temporary_column, getattr(row, column_to_encrypt, None)) | |
setattr(row, column_to_encrypt, None) | |
row.save(update_fields=[temporary_column, column_to_encrypt]) | |
def replicate_to_real(apps, schema_editor): | |
Model = apps.get_model(app_with_model, model_with_column) | |
for row in Model.objects.all(): | |
setattr(row, column_to_encrypt, getattr(row, temporary_column)) | |
row.save(update_fields=[column_to_encrypt]) | |
class Migration(migrations.Migration): | |
dependencies = [ | |
(app_with_model, '0000_whichever'), | |
] | |
operations = [ | |
# create temporary column | |
migrations.AddField( | |
model_name=model_with_column.lower(), | |
name=temporary_column, | |
field=column_field_class( | |
verbose_name=temporary_column, null=True, **column_attrs), | |
), | |
# allow null entries in the real column | |
migrations.AlterField( | |
model_name=model_with_column.lower(), | |
name=column_to_encrypt, | |
field=column_field_class( | |
verbose_name=column_to_encrypt, null=True, **column_attrs), | |
), | |
# push all data from real to temporary | |
migrations.RunPython(replicate_to_temporary), | |
# encrypt the real column (still allowing null values) | |
migrations.AlterField( | |
model_name=model_with_column.lower(), | |
name=column_to_encrypt, | |
field=encrypt(column_field_class( | |
verbose_name=column_to_encrypt, null=True, **column_attrs)), | |
), | |
# push all data from temporary to real (encrypting in the processes) | |
migrations.RunPython(replicate_to_real), | |
# remove the temporary column | |
migrations.RemoveField( | |
model_name=model_with_column.lower(), | |
name=temporary_column), | |
# disallow null values (if applicable) | |
migrations.AlterField( | |
model_name=model_with_column.lower(), | |
name=column_to_encrypt, | |
field=encrypt(column_field_class( | |
verbose_name=column_to_encrypt, null=column_null_status, | |
**column_attrs)), | |
), | |
] |
This is super helpful! I'm curious how to make this migration reversible. It seems like all RunPython
calls could just have the same function used as the reverse_with
argument (ie migrations.RunPython(replicate_to_temporary, replicate_to_temporary),
). The problem is the last AlterField
operation gets reversed first, which means copying data from the now-encrypted column tries to put encrypted binary data into a text field. Stack overflow is likely a better place for this, but I can't find the post which linked here!
Update: I initially found this gist because I was having trouble with the official example. My Model class required that the encrypt/decrypt python functions to be used as migration operations had this:
for row in Model.objects.all():
Replaced with this:
db_alias = schema_editor.connection.alias
for row in Model.inherit_objects.using(db_alias).all():
Using that tweak in the official example led to a fully reversible encryption/decryption migration. Missy Elliot would be proud!
@thismatters
I keep getting this error
Unsupported lookup 'exact' for EncryptedPhoneNumberField or join on the field not permitted, perhaps you meant exact or iexact?
--
Fill out the constants at the top and the migration dependencies.
Before you migrate, update your
models.py
to encrypt the field itself.