Django Proxy Model Permissions Do Not Appear - django

I extended Django admin site for my app to allow non-staff/superusers access. This is working just fine.
I created a proxy model for an existing model and registered it to my admin site, however, it doesn't appear for non-staff users. From the documentation I read, my understanding is that proxy models get their own permissions. I checked and these don't appear in the list of available permissions.
Here's my code in case it helps:
Normal Model
class Engagement(models.Model):
eng_type = models.CharField(max_length=5)
environment = models.CharField(max_length=8)
is_scoped = models.BooleanField()
class Meta:
ordering = ['eng_type', 'environment']
app_label = 'myapp'
Proxy Model
class NewRequests(Engagement):
class Meta:
proxy = True
app_label = 'myapp'
verbose_name = 'New Request'
verbose_name_plural = 'New Requests'
Model Admin
class NewRequestsAdmin(ModelAdmin):
pass
def queryset(self, request):
return self.model.objects.filter(is_scoped=0)
Custom Admin Registration
myapps_admin_site.register(NewRequests, NewRequestsAdmin)
I've been managing my DB with South. According to this post, you have to tamper with it a bit by following the instructions it points users to. This was a failure. My DB doesn't have a whole lot of info in it, so I uncommented South and ran a regular syncdb to rule out South. Unfortunately, this is still not working and I'm at a loss. Any help is appreciated.
Edit
This was on Django 1.4

Turns out I didn't do anything wrong. I was looking for the permissions under
myapp | New Request | Can add new request
Permissions fall under the parent model.
myapp | engagement | Can add new request

This is fixed in Django 2.2, quoting release notes:
Permissions for proxy models are now created using the content type of the proxy model rather than the content type of the concrete model. A migration will update existing permissions when you run migrate.
and docs:
Proxy models work exactly the same way as concrete models. Permissions are created using the own content type of the proxy model. Proxy models don’t inherit the permissions of the concrete model they subclass.

There is a workaround, you can see it here: https://gist.github.com/magopian/7543724
It can vary based on your django version, but the priciple is the same.
Tested with Django 1.10.1
# -*- coding: utf-8 -*-
"""Add permissions for proxy model.
This is needed because of the bug https://code.djangoproject.com/ticket/11154
in Django (as of 1.6, it's not fixed).
When a permission is created for a proxy model, it actually creates if for it's
base model app_label (eg: for "article" instead of "about", for the About proxy
model).
What we need, however, is that the permission be created for the proxy model
itself, in order to have the proper entries displayed in the admin.
"""
from __future__ import unicode_literals, absolute_import, division
import sys
from django.contrib.auth.management import _get_all_permissions
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
from django.apps import apps
from django.utils.encoding import smart_text
class Command(BaseCommand):
help = "Fix permissions for proxy models."
def handle(self, *args, **options):
for model in apps.get_models():
opts = model._meta
ctype, created = ContentType.objects.get_or_create(
app_label=opts.app_label,
model=opts.object_name.lower(),
defaults={'name': smart_text(opts.verbose_name_raw)})
for codename, name in _get_all_permissions(opts):
p, created = Permission.objects.get_or_create(
codename=codename,
content_type=ctype,
defaults={'name': name})
if created:
sys.stdout.write('Adding permission {}\n'.format(p))
How to use
create a directory /myproject/myapp/management/commands
create the file /myproject/myapp/management/__init__.py
create the file /myproject/myapp/management/commands/__init__.py
save the code above into /myproject/myapp/management/commands/fix_permissions.py
run /manage.py fix_permissions

This is a known bug in Django: https://code.djangoproject.com/ticket/11154 (check comments for some patches)

As of 2021 and Django 3+, the solution for missing permissions for proxy model is simple, just generate migrations with makemigrations:
app#e31a3ffef22c:~/app$ python manage.py makemigrations my_app
Migrations for 'main':
main/migrations/0193_myproxymodel.py
- Create proxy model MyProxyModel
I came here and wasn't really sure, what is the correct cause/solution to this problem.

For Django 1.11
This issue is related due to the wrong content_type_id in auth_permission table.
By default, it adds the content type of the base model instead of proxy model content type.

I realize this question was closed a while ago, but I'm sharing what worked for me in case it might help others.
It turns out that even though permissions for the proxy models I created were listed under the parent apps (as #chirinosky) has mentioned, and even though I granted my non-super user all permissions, it was still denied access to my proxy models through the admin.
What I had to do was workaround a known Django bug (https://code.djangoproject.com/ticket/11154) and connect to the post_syncdb signal to properly create permissions for the proxy models. The code below is modified from https://djangosnippets.org/snippets/2677/ per some of the comments on that thread.
I placed this in myapp/models.py that held my proxy models. Theoretically this can live in any of your INSTALLED_APPS after django.contrib.contenttypes because it needs to be loaded after the update_contenttypes handler is registered for the post_syncdb signal so we can disconnect it.
def create_proxy_permissions(app, created_models, verbosity, **kwargs):
"""
Creates permissions for proxy models which are not created automatically
by 'django.contrib.auth.management.create_permissions'.
See https://code.djangoproject.com/ticket/11154
Source: https://djangosnippets.org/snippets/2677/
Since we can't rely on 'get_for_model' we must fallback to
'get_by_natural_key'. However, this method doesn't automatically create
missing 'ContentType' so we must ensure all the models' 'ContentType's are
created before running this method. We do so by un-registering the
'update_contenttypes' 'post_syncdb' signal and calling it in here just
before doing everything.
"""
update_contenttypes(app, created_models, verbosity, **kwargs)
app_models = models.get_models(app)
# The permissions we're looking for as (content_type, (codename, name))
searched_perms = list()
# The codenames and ctypes that should exist.
ctypes = set()
for model in app_models:
opts = model._meta
if opts.proxy:
# Can't use 'get_for_model' here since it doesn't return
# the correct 'ContentType' for proxy models.
# See https://code.djangoproject.com/ticket/17648
app_label, model = opts.app_label, opts.object_name.lower()
ctype = ContentType.objects.get_by_natural_key(app_label, model)
ctypes.add(ctype)
for perm in _get_all_permissions(opts, ctype):
searched_perms.append((ctype, perm))
# Find all the Permissions that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
all_perms = set(Permission.objects.filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
))
objs = [
Permission(codename=codename, name=name, content_type=ctype)
for ctype, (codename, name) in searched_perms
if (ctype.pk, codename) not in all_perms
]
Permission.objects.bulk_create(objs)
if verbosity >= 2:
for obj in objs:
sys.stdout.write("Adding permission '%s'" % obj)
models.signals.post_syncdb.connect(create_proxy_permissions)
# See 'create_proxy_permissions' docstring to understand why we un-register
# this signal handler.
models.signals.post_syncdb.disconnect(update_contenttypes)

Related

how to generate builtin model permissions for non-managed models?

I have model like this:
class Venue(models.Model):
name = models.CharField(max_length=255)
class Meta:
managed = False
db_table = 'venue'
permissions = [
('change_venue', 'Can change venue'),
]
It is not managed because it already exists in the database (which was created before django project).
I want to use django's builtin model permissions, but they are not created by default. I tried to add them by changing Meta.permissions field but got an error: The permission codenamed 'change_venue' clashes with a builtin permission for model 'events.Venue'
What should I do? Just make migration and create permissions manually?
Fixed by creating permissions in App.ready hook:
from django.apps import AppConfig
from django.contrib.auth.management import create_permissions
class MyAppConfig(AppConfig):
name = 'myapp'
def ready(self):
create_permissions(self)
Don't know if this counts as valid solution tho
Edit 1
Method above didn't work for new database because models "were not ready/create" when app is ready. So I switched to post_migrate signal, and everything was fine.
Edit 2
After some time I have found global problem about why I don't have permissions and content types in the first place: I simply didn't make migrations for un-managed models. With migrations everything is fine.

Creating and setting permissions in Django

Let's say I have an app called Blog which contains Posts. I want a user to be able to add and change posts, but not delete them.
The Django docs have this example
from myapp.models import BlogPost
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
content_type = ContentType.objects.get_for_model(BlogPost)
permission = Permission.objects.create(codename='can_publish',
name='Can Publish Posts',
content_type=content_type)
I don't see how it's actually defining anything here, it just gives it a name and content type.
Django also has basic permission checking
Assuming you have an application with an app_label foo and a model named Bar, to test for basic permissions you should use:
add: user.has_perm('foo.add_bar')
change: user.has_perm('foo.change_bar')
delete: user.has_perm('foo.delete_bar')
In my app they would become:
add: user.has_perm('blog.add_post')
change: user.has_perm('blog.change_post')
delete: user.has_perm('blog.delete_post')
How do I create and add such permissions to a user (in code, not the admin)?
Defining custom permissions in code can be done via a model's meta (see duplicate):
class BlogPost(models.Model):
class Meta:
permissions = (('can_publish', 'Can Publish Posts'),)
Per user permissions should not be added in code for the most part, but may however be added as part of a migration if using south or the built-in django migrations if your version is high enough.
python manage.py schemamigration $appname $migration_description --empty
class Migration(SchemaMigration):
def forwards(self, orm):
daemon = orm.User.objects.get(username='daemon')
daemon.user_permissions.add($permission)
daemon.save()
def backwards(self, orm):
daemon = orm.User.objects.get(username='daemon')
daemon.user_permissions.remove($permission)
daemon.save()

Trouble migrating reusable django app models to use a custom user model

I need to update an existing project to Django 1.5 to take advantage of its newly available custom user model. However, I'm having trouble migrating reusable apps that contain a model with a foreign key to a user. Currently, the foreign key points to auth.User but with a custom user model, it needs to point to myapp.CustomUser. Hence, some kind of migration is needed. I can't simply create a migration file for it because its a reusable app. It wouldn't be future proof because each time the app is updated, I would need to remember to create that migration again (there might even be migration conflicts) so it's not exactly a plausible solution.
Is there a solution to this problem other than to, maybe, fork each project, add a migration file, and then use that instead?
Some code:
models.py in reusable app
from django.conf import settings
from django.db import models
UserModel = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
class ModelA(models.Model):
user = models.ForeignKey(UserModel)
models.py in my project
from django.conf import settings
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
...
settings.py in my project
AUTH_USER_MODEL = 'myapp.CustomUser'
So if the reusable app has a migration that creates a foreign key to a user, the following can be done to support Django 1.5's custom user model.
try:
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
class Migration(SchemaMigration):
def forwards(self, orm):
db.create_table('reusableapp.modela', (
('user', self.gf('django...ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])
models = {
...
# this should replace "auth.user"
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
'Meta': {'object_name': User.__name__},
}
"reusableapp.modela": {
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)})
}
}
I'm not sure if this is the best solution but it's being used in apps such as django-reversion.
However, this solution still can pose a problem if you originally started with auth.User and then changed to myapp.customuser, simply because south is honors AUTH_USER_MODEL but the migration for the custom user model hasn't been created yet. This can occur during testing. Ticket #1179 of south addresses this issue (http://south.aeracode.org/ticket/1179).

Add Object Level Permissions to Admin Interface of other APP (e.g. auth)

I use django-guardian for object level permissions. The documentation how to integrate this into own code is good:
http://packages.python.org/django-guardian/userguide/admin-integration.html
But how can I add this to models of other apps? I don't want to modify the code of e.g. django.contrib.auth.
I found a solution in django-reversion's source code. There is a helper called patch_admin(). Here is the snippet modified for django-guardian.
# Copy of django-reversion helpers.py
def patch_admin(model, admin_site=None):
"""
Enables version control with full admin integration for a model that has
already been registered with the django admin site.
This is excellent for adding version control to existing Django contrib
applications.
"""
admin_site = admin_site or admin.site
try:
ModelAdmin = admin_site._registry[model].__class__
except KeyError:
raise NotRegistered, "The model %r has not been registered with the admin site." % model
# Unregister existing admin class.
admin_site.unregister(model)
# Register patched admin class.
class PatchedModelAdmin(GuardedModelAdmin, VersionAdmin, ModelAdmin): # Remove VersionAdmin, if you don't use reversion.
pass
admin_site.register(model, PatchedModelAdmin)
from django.contrib.auth.models import Group
patch_admin(Group)

Django, BigIntegerField, and django.contrib.auth.user.id

Django now provides a BigIntegerField for use in django models (available in svn trunk and also 1.2 alpha-1 release).
I need my django.contrib.auth.user model to have a BigIntegerField as its auto-incrementing primary key, whereas it currently uses an auto-incrementing IntegerField as its primary key. Also, wherever contrib.auth.user is used as a ForeginKey, it would need to be BigIntegerField as well.
What is the best and safest way to go about achieving this?
While I'm not sure why you need a BigIntegerField on User (you must have a whole lotta users) its pretty easy to implement. First you'll need to get a database migration system like South. We'll use this to do a handful of migrations of your current data. If you don't have anything in your database then just ignore this part and skip to the end.
I would start by making a custom user class which inherits from the contrib.auth version like so:
from django.contrib.auth.models import User, UserManager
from django.db import models
class BigUser(User):
id = models.BigIntegerField(pk = True)
objects = UserManager()
#this lets you transperantly use any
#query methods that you could on User
Then use South's data-migration capability to make a copy of all of you User.objects.all() into your new BigUser model.
Then go through and ADD a foriegnkey in each model where its needed. DO NOT delete the original FK yet, otherwise you're links will be lost. After adding the new keys do another schema migration.
Then make another data migration which copies the FK's from the old User model to the new BigUser model. Migrate that data.
Then its safe to delete the old FK to the User model.
If you want to avoid changing the rest of your code to use the new field-name for the BigUser you can use the South rename-field utility (South can't auto-detect field renames so make sure to read the docs).
If you don't have any data in the database then you can simply implement the class above and drop it into your current models.
If you need help writing data-migrations you'll have to post a model or two.
Since you need something that's a "drop-in" replacement for User you'll need two more steps:
First we need to create a custom authentication back-end, this makes sure that any authentication requests go to your new model and that request.user returns BigUser and not User. Just cut and paste this snippet into a file called auth_backend.py in the same directory as settings.py:
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import ImproperlyConfigured
from django.db.models import get_model
class CustomUserModelBackend(ModelBackend):
def authenticate(self, username=None, password=None):
try:
user = self.user_class.objects.get(username=username)
if user.check_password(password):
return user
except self.user_class.DoesNotExist:
return None
def get_user(self, user_id):
try:
return self.user_class.objects.get(pk=user_id)
except self.user_class.DoesNotExist:
return None
#property
def user_class(self):
if not hasattr(self, '_user_class'):
self._user_class = get_model(*settings.CUSTOM_USER_MODEL.split('.', 2))
if not self._user_class:
raise ImproperlyConfigured('Could not get custom user model')
return self._user_class
Then in your settings.py file you need to add this back-end and set the custom user model setting ... like so:
AUTHENTICATION_BACKENDS = (
'auth_backends.CustomUserModelBackend',
)
...
CUSTOM_USER_MODEL = 'your-app-name.BigUser'
This last section of code comes from another website describing subclassing the User model.
Now all you need to do to "drop-in" in the rest of your code is to replace all of the from django.contrib.auth.models import User with from your-app-name import BigUser as User. By doing this you wont have to update any references of User with BigUser
I am weighing the option of changing the code of django.contrib.auth.models.user to include an id field as BigIntegerField primary key.
Seems to me to be the best way to go.
(I am ready to migrate the data manually via sql)