How can I use Django permissions without defining a content type or model?
Asked Answered
R

9

113

I'd like to use a permissions based system to restrict certain actions within my Django application. These actions need not be related to a particular model (e.g. access to sections in the application, searching...), so I can't use the stock permissions framework directly, because the Permission model requires a reference to an installed content type.

I could write my own permission model but then I'd have to rewrite all the goodies included with the Django permissions, such as:

I've checked some apps like django-authority and django-guardian, but they seem to provide permissions even more coupled to the model system, by allowing per-object permissions.

Is there a way to reuse this framework without having defined any model (besides User and Group) for the project?

Release answered 18/12, 2012 at 12:14 Comment(1)
Django-guardian allows the object parameter to be optional: django-guardian.readthedocs.io/en/stable/api/… - see "Global permissions"Sinistrad
I
62

Django's Permission model requires a ContentType instance.

I think one way around it is creating a dummy ContentType that isn't related to any model (the app_label and model fields can be set to any string value).

If you want it all clean and nice, you can create a Permission proxy model that handles all the ugly details of the dummy ContentType and creates "modelless" permission instances. You can also add a custom manager that filters out all Permission instances related to real models.

Intratelluric answered 18/12, 2012 at 12:27 Comment(3)
If you don't mind, I'll complete your answer with my implementation.Release
Sadly, I cannot approve as I don't have enough reputation to review your edit (it asks me for +2k). Other users are rejecting your edits, so I suggest you add it as another answer (you have my upvote!) Thanks again.Intratelluric
That's weird. It really is a completion for your answer, so it makes sense to make it an edit. Anyway, I put it in another answer.Release
P
235

For those of you, who are still searching:

You can create an auxiliary model with no database table. That model can bring to your project any permission you need. There is no need to deal with ContentType or create Permission objects explicitly.

from django.db import models
        
class RightsSupport(models.Model):
            
    class Meta:
        
        managed = False  # No database table creation or deletion  \
                         # operations will be performed for this model. 
                
        default_permissions = () # disable "add", "change", "delete"
                                 # and "view" default permissions

        permissions = ( 
            ('customer_rights', 'Global customer rights'),  
            ('vendor_rights', 'Global vendor rights'), 
            ('any_rights', 'Global any rights'), 
        )

Right after manage.py makemigrations and manage.py migrate you can use these permissions like any other.

# Decorator

@permission_required('app.customer_rights')
def my_search_view(request):
    …

# Inside a view

def my_search_view(request):
    request.user.has_perm('app.customer_rights')

# In a template
# The currently logged-in user’s permissions are stored in the template variable {{ perms }}

{% if perms.app.customer_rights %}
    <p>You can do any customer stuff</p>
{% endif %}
Puckery answered 23/6, 2016 at 10:7 Comment(13)
Nothing changed after i ran manage.py migrate... I don't see any new permissions :(Abhorrent
Did you add your app into your project (INSTALLED_APPS)?Puckery
This answer is perfect. I also []ed default_permissions, raise NotImplementedError on the model's save(), and might consider making has_*_permission() return False if the unmanaged model is truly JUST for this permission.Retaliate
Pure brilliance! Works with Django 1.11Proverb
It isn't displaying in django admin User permissions section. Any idea how to bring it there ?Giuditta
Make sure to do makemigrations then migrateFaction
I suggest adding in the Meta class the following: default_permissions = (). This will prevent Django automatically creating the default add/change/delete/view permissions for this model, which are most likely unnecessary if you're using this approach.Beguin
FYI make sure that this is defined in a file called models.py of an app listed in settings.INSTALLED_APPS or the model will not be picked up by migrations.Cinchonine
If makemigrations is not picking up your dummy model, even after you properly configured settings.INSTALLED_APPS, remember to create empty migrations python package at app level!Retha
A small gotcha with this approach is that dumpdata still includes unmanaged tables, by design, so ./manage.py dumpdata with no additional arguments will fail on the missing table.Kilkenny
'makemigrations' did not work (see comment above for the reason). 'makemigrations <myapp>' worked.Hodess
thanks for sharing, it works! Although still looks hacky and not intuitive, as it still makes django add a new row to django_content_type referring to a model that doesn't exist in the database.Muldon
Take care that you can't inherit the default_permissions attribute. If you want to build some kind of mixin, this won't work by design: code.djangoproject.com/ticket/29386Gymnasiarch
I
62

Django's Permission model requires a ContentType instance.

I think one way around it is creating a dummy ContentType that isn't related to any model (the app_label and model fields can be set to any string value).

If you want it all clean and nice, you can create a Permission proxy model that handles all the ugly details of the dummy ContentType and creates "modelless" permission instances. You can also add a custom manager that filters out all Permission instances related to real models.

Intratelluric answered 18/12, 2012 at 12:27 Comment(3)
If you don't mind, I'll complete your answer with my implementation.Release
Sadly, I cannot approve as I don't have enough reputation to review your edit (it asks me for +2k). Other users are rejecting your edits, so I suggest you add it as another answer (you have my upvote!) Thanks again.Intratelluric
That's weird. It really is a completion for your answer, so it makes sense to make it an edit. Anyway, I put it in another answer.Release
R
56

Following Gonzalo's advice, I used a proxy model and a custom manager to handle my "modelless" permissions with a dummy content type.

from django.db import models
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType


class GlobalPermissionManager(models.Manager):
    def get_queryset(self):
        return super(GlobalPermissionManager, self).\
            get_query_set().filter(content_type__name='global_permission')


class GlobalPermission(Permission):
    """A global permission, not attached to a model"""

    objects = GlobalPermissionManager()

    class Meta:
        proxy = True

    def save(self, *args, **kwargs):
        ct, created = ContentType.objects.get_or_create(
            name="global_permission", app_label=self._meta.app_label
        )
        self.content_type = ct
        super(GlobalPermission, self).save(*args, **kwargs)
Release answered 19/12, 2012 at 12:9 Comment(9)
thanks for the code, it would be nice to also show an example on how to use this code.Trichocyst
where should that model permission live?Hewitt
@MiratCanBayrak, I believe it suppose too live somewhere in your core app.Phrygian
To create a GlobalPermission: from app.models import GlobalPermission gp = GlobalPermission.objects.create(codename='can_do_it', name='Can do it') Once this is run you can add that permission to users/group like any other permission.Glanti
@MiratCanBayrak I'd rather put it at the end of app's models.py as it contains code which influence database.Dissolvent
@JulienGrenier The code breaks in Django 1.8: FieldError: Cannot resolve keyword 'name' into field. Choices are: app_label, id, logentry, model, permission.Dissolvent
I got the same error as @Dissolvent , and I'm on django 1.8.2, after I remove the name from the parameter, I got IntegrityError: NOT NULL constraint failed: auth_permission.content_type_id, I can't wrap my head around this, can @Release maybe update your implementation?Frightfully
What's the purpose of filtering in get_query_set?Thorin
Warning: Newer versions of Django (at least 1.10) need to override the method "get_queryset" (note the lack of _ between the words "query" and "set).Knickknack
V
11

Fix for Chewie's answer in Django 1.8, which as been requested in a few comments.

It says in the release notes:

The name field of django.contrib.contenttypes.models.ContentType has been removed by a migration and replaced by a property. That means it’s not possible to query or filter a ContentType by this field any longer.

So it's the 'name' in reference in ContentType that the uses not in GlobalPermissions.

When I fix it I get the following:

from django.db import models
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType


class GlobalPermissionManager(models.Manager):
    def get_queryset(self):
        return super(GlobalPermissionManager, self).\
            get_queryset().filter(content_type__model='global_permission')


class GlobalPermission(Permission):
    """A global permission, not attached to a model"""

    objects = GlobalPermissionManager()

    class Meta:
        proxy = True
        verbose_name = "global_permission"

    def save(self, *args, **kwargs):
        ct, created = ContentType.objects.get_or_create(
            model=self._meta.verbose_name, app_label=self._meta.app_label,
        )
        self.content_type = ct
        super(GlobalPermission, self).save(*args)

The GlobalPermissionManager class is unchanged but included for completeness.

If using Django admin to create/edit these permissions, think of removing the content_type field to avoid confusion:

class GlobalPermissionAdmin(admin.ModelAdmin):
    list_display = ("name", "codename")
    exclude = ("content_type",)

admin.site.register(GlobalPermission, GlobalPermissionAdmin)
Velodrome answered 24/6, 2015 at 15:50 Comment(2)
This still doesn't fix it for django 1.8 as in time of syncdb django asserts that the "name" field cannot be null.Festus
It worked for me, but I'm not using migrations due to non-django legacy stuff still in my project. Are you upgrading from a previous django, because there isn't supposed to be a name field in 1.8Velodrome
N
5

This is alternative solution. First ask yourself: Why not create a Dummy-Model which really exists in DB but never ever gets used, except for holding permissions? That's not nice, but I think it is valid and straight forward solution.

from django.db import models

class Permissions(models.Model):

    can_search_blue_flower = 'my_app.can_search_blue_flower'

    class Meta:
        permissions = [
            ('can_search_blue_flower', 'Allowed to search for the blue flower'),
        ]

Above solution has the benefit, that you can use the variable Permissions.can_search_blue_flower in your source code instead of using the literal string "my_app.can_search_blue_flower". This means less typos and more autocomplete in IDE.

Nasty answered 21/12, 2017 at 12:18 Comment(2)
Does using managed=False not let you use Permissions.can_search_blue_flower for some reason?Immaculate
@SamBobel yes, you could be right. I guess I just tried "abstract" the last time.Nasty
T
2

You can use the proxy model for this with a dummy content type.

from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType


class CustomPermission(Permission):

    class Meta:
        proxy = True

    def save(self, *args, **kwargs):
        ct, created = ContentType.objects.get_or_create(
            model=self._meta.verbose_name, app_label=self._meta.app_label,
        )
        self.content_type = ct
        super(CustomPermission, self).save(*args)

Now you can create the permission with just name and codename of the permission from the CustomPermission model.

 CustomPermission.objects.create(name='Can do something', codename='can_do_something')

And you can query and display only the custom permissions in your templates like this.

 CustomPermission.objects.filter(content_type__model='custom permission')
Twocolor answered 27/5, 2020 at 14:47 Comment(0)
R
2

For my part, for any larger project, I find it useful to have a generic app that isn't really part of my project's data model per se - I typically call it "projectlibs". It's a simple django app where I put things like fixtures for imports, templatetags that can be reused for multiple apps, etc. Some of it is template stuff I find myself re-using often, so the added benefit of having that type of stuff in an app is that it's reusable for other projects.

So inside that projectlibs/models.py, you could:

You could create that "meta app", in essence, and assign the content_type to some dummy class:

class UserRightsSupport(models.Model):
    class Meta:
        default_permissions = ()  # disable defaults add, delete, view, change perms
        permissions = (
            ("perm_name", "Verbose description"),
        )
Roscoeroscommon answered 1/4, 2021 at 13:20 Comment(0)
R
0

All answers are bad for me except this:

content_type = ContentType.objects.get_for_model(Permission)

Permission.objects.create(
    content_type=content_type,
    name='...', codename='...',
)

which handles model-less permissions without adding new models, but by adding new values.

Riorsson answered 1/10, 2021 at 16:30 Comment(4)
That isn't model-less, it just uses the Permission content type (model).Biscay
@soxwithMonica model-less because a Permission model already exists so it is about adding new values, not models - that's why it satisfies without defining a content type or model.Popinjay
I figured that, but it was better to state it explicitly anyway.Biscay
As simple as this approach looks, would have been the best answer but, adding permissions for your users on the Permission model is too risky. A little mistake can cause chaosIronstone
L
0

I think that User model substitution do the trick (see Django documentation: https://docs.djangoproject.com/en/4.1/topics/auth/customizing/#substituting-a-custom-user-model)

Next on that model we can add Meta class with permissions and at the end we need to add our User model in settings.py.

Here is my example of user.py file:

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):

    class Meta:
        default_permissions = []  # disable "add", "change", "delete"
        # and "view" default permissions

        permissions = [
            ('customer_rights', 'Global customer rights'),
            ('vendor_rights', 'Global vendor rights'), 
            ('any_rights', 'Global any rights'),
        ]

and in settings.py:

AUTH_USER_MODEL = 'myapp.User'
Libre answered 7/10, 2022 at 21:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.