raphaelm / django-scopes

Safely separate multiple tenants in a Django database
Apache License 2.0
227 stars 15 forks source link

Usage in Admin #10

Open JackAtOmenApps opened 4 years ago

JackAtOmenApps commented 4 years ago

There seems to be a problem with using this package along with django's admin. If I try to view the listing for a scoped model, I get:

"Database error Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user."

If I click "+" to add a scoped model instance, I get the following error and traceback:

django_scopes.exceptions.ScopeError django_scopes.exceptions.ScopeError: A scope on dimension(s) site needs to be active for this query.

Traceback (most recent call last)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/staticfiles/handlers.py", line 65, in __call__
return self.application(environ, start_response)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/wsgi.py", line 141, in __call__
response = self.get_response(request)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/base.py", line 75, in get_response
response = self._middleware_chain(request)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/exception.py", line 36, in inner
response = response_for_exception(request, exc)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/exception.py", line 90, in response_for_exception
response = handle_uncaught_exception(request, get_resolver(get_urlconf()), sys.exc_info())
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/exception.py", line 125, in handle_uncaught_exception
return debug.technical_500_response(request, *exc_info)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django_extensions/management/technical_response.py", line 37, in null_technical_500_response
six.reraise(exc_type, exc_value, tb)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/six.py", line 695, in reraise
raise value.with_traceback(tb)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
response = get_response(request)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
response = self.process_exception_by_middleware(e, request)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.7/contextlib.py", line 74, in inner
return func(*args, **kwds)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 606, in wrapper
return self.admin_site.admin_view(view)(*args, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
response = view_func(request, *args, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/sites.py", line 223, in inner
return view(request, *args, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1634, in add_view
return self.changeform_view(request, None, form_url, extra_context)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/utils/decorators.py", line 45, in _wrapper
return bound_method(*args, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1522, in changeform_view
return self._changeform_view(request, object_id, form_url, extra_context)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1576, in _changeform_view
formsets, inline_instances = self._create_formsets(request, form.instance, change=False)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 1932, in _create_formsets
for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 794, in get_formsets_with_inlines
yield inline.get_formset(request, obj), inline
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 2036, in get_formset
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 330, in get_fieldsets
return [(None, {'fields': self.get_fields(request, obj)})]
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 321, in get_fields
form = self._get_form_for_get_fields(request, obj)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 2121, in _get_form_for_get_fields
return self.get_formset(request, obj, fields=None).form
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 2118, in get_formset
return inlineformset_factory(self.parent_model, self.model, **defaults)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/forms/models.py", line 1077, in inlineformset_factory
FormSet = modelformset_factory(model, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/forms/models.py", line 875, in modelformset_factory
error_messages=error_messages, field_classes=field_classes)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/forms/models.py", line 551, in modelform_factory
return type(form)(class_name, (form,), form_class_attrs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/forms/models.py", line 256, in __new__
apply_limit_choices_to=False,
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/forms/models.py", line 176, in fields_for_model
formfield = formfield_callback(f, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 156, in formfield_for_dbfield
formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/contrib/admin/options.py", line 243, in formfield_for_foreignkey
return db_field.formfield(**kwargs)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/db/models/fields/related.py", line 956, in formfield
**kwargs,
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/db/models/fields/related.py", line 419, in formfield
return super().formfield(**defaults)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/db/models/fields/__init__.py", line 897, in formfield
return form_class(**defaults)
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/forms/models.py", line 1178, in __init__
self.queryset = queryset
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django/forms/models.py", line 1203, in _set_queryset
self._queryset = None if queryset is None else queryset.all()
File "/home/myproject/.local/share/virtualenvs/myproject_district-6OFqqZqm/lib/python3.7/site-packages/django_scopes/manager.py", line 15, in error
', '.join(self.missing_scopes)
django_scopes.exceptions.ScopeError: A scope on dimension(s) site needs to be active for this query.

I don't see any admin-specific notes in the documentation (except creating commands), and I don't see a way around this.

Surely I'm missing something simple. Any recommendations?

raphaelm commented 4 years ago

Since we don't use django admin on the projects we built this for, we never tested it or prepared for it. I assume the easiest way is to build a middleware that disables scopes entirely for requests to admin. If you want to go down this route, feel free to add a note to the documentation!

limugob commented 4 years ago

I think it is not easy to determine in middleware if a request is for the admin interface. (there can be more than one.)

So i wrote a class for ModelAdmin, which change the get_queryset like this:

from django_scopes import scopes_disabled

class DisabledScopesAdminMixin:
    def get_queryset(self, request):
        with scopes_disabled():
            return super().get_queryset(request)
cboden commented 4 years ago

Thanks for the snippet @limugob.

I have an odd scenario where it works on the listing but not the change aspect of the admin model. Any ideas? Is there another method of the AdminModel that may need to have its scope disabled?

cboden commented 4 years ago

I found another approach inspired by some code I found in pretix:

from django.http import HttpRequest
from django_scopes import scopes_disabled
from django.urls import get_script_prefix

def ignore_scopes_in_admin_middleware(get_response, admin_path: str="admin"):
    def middleware(request: HttpRequest):
        if request.path.startswith(get_script_prefix() + admin_path):
            with scopes_disabled():
                return get_response(request)

        return get_response(request)

    return middleware
raphaelm commented 4 years ago

@limugob's solution probably does not work since scopes are disabled while the queryset is created, but not when it is lazily executed. @cboden's approach should work reliably (disables scoping for the admin entirely). I would make sure that your startswith argument ends with a / though, otherwise an endpoint like /adminimization would also be exempt from scoping.

limugob commented 4 years ago

Sorry my approach worked only for the overview of objects, not for editing. I changed it to https://github.com/limugob/big_eggs/blob/master/big_eggs/tenants/middleware.py