Skip to content

Admin

Admin helpers for multi-tenant projects.

This module contains logic to restrict access to certain Django admin models so they are only visible and editable from the master tenant. The typical use-case might be the project where a subset of models (for example global configuration, billing plans, or shared resources) should only be managed at a single, centrally-administered tenant and hidden from per-tenant admin interfaces.

How it works

  • The TenantRestrictAdminMixin is mixed into existing ModelAdmin classes at import time for selected models.
  • For those models the mixin overrides permission checks and model visibility so that only the configured master tenant may see and operate on them.
  • Models are selected for restriction if any of the following are true:
  • the app config defines master_managed = True
  • the model class defines master_managed = True
  • the model class defines tenant_managed = False (i.e. not tenant-scoped)

Configuration

  • The master tenant name is read from settings.MASTER_TENANT_NAME.
  • The list of apps to inspect comes from :func:get_custom_apps which allows consumers to control which installed apps are evaluated.

Notes for maintainers

  • This module runs at import time and mutates admin.site by unregistering and re-registering ModelAdmin classes. Import order matters: it is important this module is imported after apps have been loaded and their ModelAdmin registrations have occurred. Thus django_omnitenant must be placed after all the custom defined apps in the project in the INSTALLED_APPS.

TenantRestrictAdminMixin

Bases: ModelAdmin

A small mixin that restricts admin access to the master tenant.

This mixin is intentionally minimal: it overrides the standard ModelAdmin permission checks to return permissive results only when the current request is served under the master tenant. When the request's tenant is not the master tenant, the mixin denies visibility and all CRUD permissions so the model is hidden from the non-master tenant's admin UI.

Implementation details

  • _is_master_tenant: helper that inspects request.tenant and compares its name to settings.MASTER_TENANT_NAME. It is small and isolated so it can be overridden in tests if necessary.
  • Each of the permission hooks used by Django admin (get_model_perms, has_*_permission) delegates to _is_master_tenant to make the logic explicit and consistent.

Override considerations

If a project needs finer-grained control (for example allowing view-only access to non-master tenants) you can extend this mixin and override the individual has_*_permission methods.

Source code in django_omnitenant/admin.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class TenantRestrictAdminMixin(admin.ModelAdmin):
    """A small mixin that restricts admin access to the master tenant.

    This mixin is intentionally minimal: it overrides the standard
    ModelAdmin permission checks to return permissive results only when
    the current request is served under the master tenant. When the
    request's tenant is not the master tenant, the mixin denies
    visibility and all CRUD permissions so the model is hidden from the
    non-master tenant's admin UI.

    Implementation details
    ----------------------
    - ``_is_master_tenant``: helper that inspects ``request.tenant`` and
      compares its ``name`` to ``settings.MASTER_TENANT_NAME``. It is
      small and isolated so it can be overridden in tests if necessary.
    - Each of the permission hooks used by Django admin
      (``get_model_perms``, ``has_*_permission``) delegates to
      ``_is_master_tenant`` to make the logic explicit and consistent.

    Override considerations
    -----------------------
    If a project needs finer-grained control (for example allowing
    view-only access to non-master tenants) you can extend this mixin
    and override the individual ``has_*_permission`` methods.
    """

    def _is_master_tenant(self, request):
        """Return ``True`` when the current request belongs to the master tenant.

        Args:
            request: the HTTP request object passed by Django admin.

        Returns:
            ``True`` if ``request.tenant.name`` equals
            ``settings.MASTER_TENANT_NAME``; ``False`` otherwise.
        """

        tenant: BaseTenant = request.tenant
        return tenant.name == settings.MASTER_TENANT_NAME

    def get_model_perms(self, request):
        """Return the model permissions dictionary for the current request.

        When not in the master tenant an empty dict is returned which
        causes Django admin to hide the model from the index/listing.
        """

        if self._is_master_tenant(request):
            return super().get_model_perms(request)
        return {}

    def has_module_permission(self, request):
        """Whether the model module should be visible in the admin index."""

        return self._is_master_tenant(request)

    def has_view_permission(self, request, obj=None):
        """Whether ``request`` can view instances of this model."""

        return self._is_master_tenant(request)

    def has_add_permission(self, request):
        """Whether ``request`` can add new instances of this model."""

        return self._is_master_tenant(request)

    def has_change_permission(self, request, obj=None):
        """Whether ``request`` can modify the given object (or any object)."""

        return self._is_master_tenant(request)

    def has_delete_permission(self, request, obj=None):
        """Whether ``request`` can delete the given object (or any object)."""

        return self._is_master_tenant(request)

get_model_perms(request)

Return the model permissions dictionary for the current request.

When not in the master tenant an empty dict is returned which causes Django admin to hide the model from the index/listing.

Source code in django_omnitenant/admin.py
85
86
87
88
89
90
91
92
93
94
def get_model_perms(self, request):
    """Return the model permissions dictionary for the current request.

    When not in the master tenant an empty dict is returned which
    causes Django admin to hide the model from the index/listing.
    """

    if self._is_master_tenant(request):
        return super().get_model_perms(request)
    return {}

has_module_permission(request)

Whether the model module should be visible in the admin index.

Source code in django_omnitenant/admin.py
96
97
98
99
def has_module_permission(self, request):
    """Whether the model module should be visible in the admin index."""

    return self._is_master_tenant(request)

has_view_permission(request, obj=None)

Whether request can view instances of this model.

Source code in django_omnitenant/admin.py
101
102
103
104
def has_view_permission(self, request, obj=None):
    """Whether ``request`` can view instances of this model."""

    return self._is_master_tenant(request)

has_add_permission(request)

Whether request can add new instances of this model.

Source code in django_omnitenant/admin.py
106
107
108
109
def has_add_permission(self, request):
    """Whether ``request`` can add new instances of this model."""

    return self._is_master_tenant(request)

has_change_permission(request, obj=None)

Whether request can modify the given object (or any object).

Source code in django_omnitenant/admin.py
111
112
113
114
def has_change_permission(self, request, obj=None):
    """Whether ``request`` can modify the given object (or any object)."""

    return self._is_master_tenant(request)

has_delete_permission(request, obj=None)

Whether request can delete the given object (or any object).

Source code in django_omnitenant/admin.py
116
117
118
119
def has_delete_permission(self, request, obj=None):
    """Whether ``request`` can delete the given object (or any object)."""

    return self._is_master_tenant(request)