Opened 6 years ago

Last modified 11 months ago

#29843 assigned Cleanup/optimization

Create permissions using migration operations rather than using the post_migrate signal

Reported by: Petter Strandmark Owned by: Arthur Rio
Component: contrib.auth Version: dev
Severity: Normal Keywords: contenttypes permissions post_migrate
Cc: Petter Strandmark, Arthur Rio, Ryan Hiebert Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: yes Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

We have encountered this problem when deploying our app to a new database.

The easiest way to demonstrate the problem is with this repository: https://github.com/PetterS/django-demo-app

First clone the repo and execute

pipenv install

Then in a clean repository:

$ python manage.py migrate auth
$ python manage.py migrate myapp

This fails with "django.contrib.auth.models.DoesNotExist: Permission matching query does not exist."

Indeed, I can verify that the sqlite database does not contain any custom permissions. But showmigrations tells me that "0001_initial" and "0002_auto_20181012_1052" have been applied!

$ python manage.py showmigrations
...
myapp
 [X] 0001_initial
 [X] 0002_auto_20181012_1052
 [ ] 0003_auto_20181012_1054

On the other hand, in a clean repository the following succeeds:

$ python manage.py migrate auth
$ python manage.py migrate myapp 0001_initial
$ python manage.py migrate myapp 0002_auto_20181012_1052
$ python manage.py migrate myapp 0003_auto_20181012_1054
$ python manage.py migrate myapp

So the result of the migrations adding the permission is not visible until after the migrate command finished, even if there are other migrations coming after.

In a clean repository:

$ python manage.py migrate auth
$ python manage.py migrate myapp 0001_initial

"can_smell" is now a permission in the DB.

I think this bug is pretty severe, since running migrate on a new DB will put the database in a state that is not very easy to recover from. The migrations show as applied, when in fact the permissions have not been created.

Change History (17)

comment:1 by Tim Graham, 6 years ago

Component: Uncategorizedcontrib.auth
Summary: Migrations involving permissions (in Meta class of model) are not applied immediatelyCreate permissions using migration operations rather than using the post_migrate signal
Triage Stage: UnreviewedAccepted
Type: BugCleanup/optimization

There's a recent discussion that describes the same problem for contenttypes.

comment:2 by Petter Strandmark, 6 years ago

Cc: Petter Strandmark added

comment:3 by Arthur Rio, 6 years ago

Cc: Arthur Rio added

comment:4 by Arthur Rio, 6 years ago

Keywords: contenttypes permissions post_migrate added
Owner: changed from nobody to Arthur Rio
Status: newassigned

I don't think we can solve the ticket for permissions unless we fix it for content types as well. The creation/deletion/renaming of permissions and content types should happen in the makemigrations phase as opposed to using a post_migrate signal.

I suggest renaming this ticket to "Manage content types and permissions using migration operations rather than using the post_migrate signal", what do you think?

The approach would be:

  • Add a pre_makemigrations_write signal, triggered right before the migrations are written to file
  • Content Type would register to that signal and insert operations if a model is created, deleted or renamed. If any operation is inserted, it would add itself to the dependencies of the migration
  • Add a post_contenttypes_insert_migrations signal, triggered if any content type operation is inserted
  • Permissions would register to that signal and insert operations if a model is created or deleted, or if any of the permissions of the model have changed (default or custom). If any operation is inserted, it would add itself to the dependencies of the migration

That way all the operations are part of the migration files and subsequent migration operations can use the new or updated permissions/content types. Also, it would handle having only django.contrib.contenttypes setup in the INSTALLED_APPS (i.e. not django.contrib.auth). I might be missing some edge cases, so any feedback would be appreciated.

Finally, I'm not sure yet how to handle adding django.contrib.auth and django.contrib.contenttypes to the INSTALLED_APPS after migrations were already created for other models, but I think that we could keep the post_migrate signal in place to perform insert operations if 0001_initial is part of the plan.

comment:5 by Simon Charette, 6 years ago

Finally, I'm not sure yet how to handle adding django.contrib.auth and django.contrib.contenttypes to the INSTALLED_APPS after migrations were already created for other models, but I think that we could keep the post_migrate signal in place to perform insert operations if 0001_initial is part of the plan.

That's one of the reasons why RenameContentType operations are injected in the plan on pre_migrate and not baked into migration files. We must account for the fact these apps can be installed or uninstalled at any time in the state of a project.

Another reason why operations baking is problematic is migration squashing and optimizations which the operation injection technique completely works around. I don't want to deter you from trying but I spent a large amount of time working on the operation injection approach in #24067 and #24100 and it seemed like the best approach at least at that time. What's the rationale for not using a similar approach for CreateContentType, CreatePermission and RenamePermission? Someone give a shot at permission renaming in #27489 and it seemed to be working fine.

Something to keep in mind as well is that an automatically generated DeleteContentType operation could lead to data loss because of cascade deletion of foreign references. That's the reason behind the interactive remove_stale_contenttypes command. IMO that's a no go.

While I'm not a big fan of signals myself I can see how allowing third party apps to bake operations could be useful. For example, contrib.postgres could bake a CreateExtension if it detects an HStoreField is first added to a project state. I just don't think this is necessary to solve this issue as the operation injection approach seem to be working fine.

comment:6 by Petter Strandmark, 6 years ago

As a workaround, it would be helpful to have a "--one-at-a-time" flag for the migrate command that runs one (the next) migration and then exists. That is what we are doing manually now.

comment:7 by Arthur Rio, 6 years ago

That's one of the reasons why RenameContentType operations are injected in the plan on pre_migrate and not baked into migration files. We must account for the fact these apps can be installed or uninstalled at any time in the state of a project.
Another reason why operations baking is problematic is migration squashing and optimizations which the operation injection technique completely works around. I don't want to deter you from trying but I spent a large amount of time working on the operation injection approach in #24067 and #24100 and it seemed like the best approach at least at that time. What's the rationale for not using a similar approach for CreateContentType, CreatePermission and RenamePermission? ​Someone give a shot at permission renaming in #27489 and it seemed to be working fine.

The rationale was just to try to make things a bit less magical and show the set of changes to the user instead of doing the injection behind the scenes, but I hadn't considered the squashing and optimizations yet, thanks for pointing that out. Thank you also for the references to existing PRs that I can use as resources while working on this ticket.
It seems that the main issue blocking #27489 was to make sure the permission operations injection happened after the contenttypes one. What do you think about triggering a post_contenttypes_operations_injection signal so that django.contrib.auth can register to it and do its create/update permission injection then? The AlterModelOptions operations injection would happen in the pre_migrate though.

Something to keep in mind as well is that an automatically generated DeleteContentType operation could lead to data loss because of cascade deletion of foreign references. That's the reason behind the interactive remove_stale_contenttypes command. IMO that's a no go.

Sounds reasonable. Same for permissions I guess?

While I'm not a big fan of signals myself I can see how allowing third party apps to bake operations could be useful. For example, contrib.postgres could bake a CreateExtension if it detects an HStoreField is first added to a project state. I just don't think this is necessary to solve this issue as the operation injection approach seem to be working fine.

Thank you very much for your input, I will try the pre_migrate approach.

comment:8 by Simon Charette, 6 years ago

What do you think about triggering a post_contenttypes_operations_injection signal so that django.contrib.auth can register to it and do its create/update permission injection then?

This could work but an alternative could be to add a system check or raise ImproperlyConfigured if django.contrib.auth is installed before django.contrib.contenttypes.

If this assertion holds then the pre_migrate signal registered by auth is guaranteed to run after the one registered by contenttypes as it was registered after. That would allow the auth operation injection logic to look for AddContentType and RenameContentType operations in the plan and insert AddPermission and RenamePermission after them.

We already document and perform similar checks for middleware ordering so I think doing it for INSTALLED_APPS could work as well. Thing is this check would fail for most of the projects at first since the project template has been ordering them the other way around for a while. I still think this is worth enforcing a form of ordering for installed apps for this purpose though and I'd volunteer to work on a PR to tackle this.

Whether we choose a system check or an ImproperlyConfigured preceded by a period deprecation warning we should still register the post_migrate signals for a few releases if apps are not ordered appropriately. This would allow the contenttypes creation and permissions to still work in most of the cases until the deprecation ends of if some users ignore or silence the system check.

Thoughts?

comment:9 by Simon Charette, 6 years ago

Another alternative could be to introduce a post_operation signal where the sender would be the operation class, operation the Operation instance and we could include the from_state and to_state as well.

The content types app could register a signal receiver for CreateModel using post_operation.connect(sender=CreateModel, receiver) and return [CreateContentType(...)]. The executor would then execute the collected operations. Then auth app could register a post_operation.connect(sender=CreateContentType, receiver) and return [CreatePermission] which the executor would execute as well.

That would allow us get rid of the nasty plan injection logic and expose a somewhat reusable interface for third party apps all that without enforcing INSTALLED_APPS ordering for now.

comment:10 by Arthur Rio, 6 years ago

I'm in favor of the second approach since it will make the release of the patch much simpler and also provide a more generic/extensible approach to the problem.

Thank you for the great suggestions.

comment:11 by Asif Saifuddin Auvi, 6 years ago

Patch needs improvement: set
Version: 2.1master

comment:12 by Jacob Walls, 4 years ago

Patch needs improvement: unset

comment:13 by Taylor H, 4 years ago

Hi everyone 👋🏻

Copying the following from the mailing list for cross-reference (Arthur and I are working together). Hoping to get some feedback from everyone .

After some experiments and discussions it felt like while the implementation might solve the initial problem, it's a bit under the hood and will be hard to debug if something goes wrong. The idea was to inject operations at the time of running migrate.

So... we went back to the idea of having hooks during the makemigrations process instead, so that the operations would be written to the migration files, which would make it more explicit and less risky. Here is a first draft of how it would look like: https://github.com/django/django/pull/14229.

  1. We know that the makemigrations process is complicated, so before we invest more time down that path, is there something obvious we might be missing?
  2. What do you think of this approach with hooks (pre and post "add_operation")?
  3. Do you think it would be a useful feature for other third party apps as well (not just content types and permissions)?

comment:14 by Jacob Walls, 3 years ago

Has patch: set

comment:15 by Jacob Walls, 3 years ago

Needs tests: set

comment:16 by Mariusz Felisiak, 3 years ago

Needs documentation: set
Patch needs improvement: set

comment:17 by Ryan Hiebert, 11 months ago

Cc: Ryan Hiebert added
Note: See TracTickets for help on using tickets.
Back to Top