Last active
August 11, 2024 10:48
-
-
Save thismatters/53787f2d021fa5a1df640cd7b98d1185 to your computer and use it in GitHub Desktop.
Migrating existing columns to use django-cryptography
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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)), | |
), | |
] |
@thismatters
I keep getting this error
Unsupported lookup 'exact' for EncryptedPhoneNumberField or join on the field not permitted, perhaps you meant exact or iexact?
--
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 thereverse_with
argument (iemigrations.RunPython(replicate_to_temporary, replicate_to_temporary),
). The problem is the lastAlterField
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:
Replaced with this:
Using that tweak in the official example led to a fully reversible encryption/decryption migration. Missy Elliot would be proud!