Skip to content

Instantly share code, notes, and snippets.

@thismatters
Last active March 5, 2024 00:07
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save thismatters/53787f2d021fa5a1df640cd7b98d1185 to your computer and use it in GitHub Desktop.
Save thismatters/53787f2d021fa5a1df640cd7b98d1185 to your computer and use it in GitHub Desktop.
Migrating existing columns to use django-cryptography
# -*- 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
Copy link
Author

thismatters commented Sep 9, 2018

Fill out the constants at the top and the migration dependencies.

Before you migrate, update your models.py to encrypt the field itself.

@hamx0r
Copy link

hamx0r commented Jun 8, 2021

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment