I'm developing a Django app that will have two administration backends. One for daily use by "normal" users and the default one for more advanced tasks and for the developers.
The application uses some custom permissions but none of the default ones. So I'm currently looking for a way to remove the default permissions, or at least a way to hide them from the "daily" admin backend without large modifications.
UPDATE: Django 1.7 supports the customization of default permissions
Original Answer
The following is valid for Django prior to version 1.7
This is standard functionality of the auth contrib application.
It handles the post_syncdb signal and creates the permissions (the standard 3: add, change, delete, plus any custom ones) for each model; they are stored in the auth_permission table in the database.
So, they will be created each time you run the syncdb management command
You have some choices. None is really elegant, but you can consider:
Dropping the auth contrib app and provide your own authentication backend.
Consequences -> you will lose the admin and other custom apps built on top of the auth User model, but if your application is highly customized that could be an option for you
Overriding the behaviour of the post_syncdb signal inside the auth app (inside \django\contrib\auth\management__init__.py file)
Consequences -> be aware that without the basic permissions the Django admin interface won't be able to work (and maybe other things as well).
Deleting the basic permissions (add, change, delete) for each model inside the auth_permission table (manually, with a script, or whatever).
Consequences -> you will lose the admin again, and you will need to delete them each time you run syncdb.
Building your own Permission application/system (with your own decorators, middlewares, etc..) or extending the existing one.
Consequences -> none, if you build it well - this is one of the cleanest solutions in my opinion.
A final consideration: changing the contrib applications or Django framework itself is never considered a good thing: you could break something and you will have hard times if you will need to upgrade to a newer version of Django.
So, if you want to be as clean as possibile, consider rolling your own permission system, or extending the standard one (django-guardian is a good example of an extension to django permissions). It won't take much effort, and you can build it the way it feels right for you, overcoming the limitations of the standard django permission system. And if you do a good work, you could also consider to open source it to enable other people using/improving your solution =)
I struggled with this same problem for a while and I think I've come up with a clean solution. Here's how you hide the permissions for Django's auth app:
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django import forms
from django.contrib.auth.models import Permission
class MyGroupAdminForm(forms.ModelForm):
class Meta:
model = MyGroup
permissions = forms.ModelMultipleChoiceField(
Permission.objects.exclude(content_type__app_label='auth'),
widget=admin.widgets.FilteredSelectMultiple(_('permissions'), False))
class MyGroupAdmin(admin.ModelAdmin):
form = MyGroupAdminForm
search_fields = ('name',)
ordering = ('name',)
admin.site.unregister(Group)
admin.site.register(MyGroup, MyGroupAdmin)
Of course it can easily be modified to hide whatever permissions you want. Let me know if this works for you.
A new feature introduced in Django 1.7 is the ability to define the default permissions. As stated in the documentation if you set this to empty none of the default permissions will be created.
A working example would be:
class Blar1(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, unique = True, blank = False, null = False, verbose_name= "Name")
class Meta:
default_permissions = ()
ShadowCloud gave a good rundown. Here's a simple way to accomplish your goal.
Add these line in your admin.py:
from django.contrib.auth.models import Permission
admin.site.register(Permission)
You can now add/change/delete permissions in the admin. Remove the unused ones and when you have what you want, go back and remove these two lines from admin.py.
As was mentioned by others, a subsequent syncdb will put everything back.
Built on top of the solution by #pmdarrow, I've come up with a relatively clean solution to patch the Django admin views.
See: https://gist.github.com/vdboor/6280390
It extends the User and Group admin to hide certain permissions.
You can't easily delete those permissions (so that syncdb won't put them back), but you can hide them from the admin interface. The idea is, as described by others, to override the admin forms but you have to do this for both users and groups.
Here is the admin.py with the solution:
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User, Group
from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.contrib.auth.forms import UserChangeForm
#
# In the models listed below standard permissions "add_model", "change_model"
# and "delete_model" will be created by syncdb, but hidden from admin interface.
# This is convenient in case you use your own set of permissions so the list
# in the admin interface wont be confusing.
# Feel free to add your models here. The first element is the app name (this is
# the directory your app is in) and the second element is the name of your model
# from models.py module of your app (Note: both names must be lowercased).
#
MODELS_TO_HIDE_STD_PERMISSIONS = (
("myapp", "mymodel"),
)
def _get_corrected_permissions():
perms = Permission.objects.all()
for app_name, model_name in MODELS_TO_HIDE_STD_PERMISSIONS:
perms = perms.exclude(content_type__app_label=app_name, codename='add_%s' % model_name)
perms = perms.exclude(content_type__app_label=app_name, codename='change_%s' % model_name)
perms = perms.exclude(content_type__app_label=app_name, codename='delete_%s' % model_name)
return perms
class MyGroupAdminForm(forms.ModelForm):
class Meta:
model = Group
permissions = forms.ModelMultipleChoiceField(
_get_corrected_permissions(),
widget=admin.widgets.FilteredSelectMultiple(('permissions'), False),
help_text = 'Hold down "Control", or "Command" on a Mac, to select more than one.'
)
class MyGroupAdmin(GroupAdmin):
form = MyGroupAdminForm
class MyUserChangeForm(UserChangeForm):
user_permissions = forms.ModelMultipleChoiceField(
_get_corrected_permissions(),
widget=admin.widgets.FilteredSelectMultiple(('user_permissions'), False),
help_text = 'Hold down "Control", or "Command" on a Mac, to select more than one.'
)
class MyUserAdmin(UserAdmin):
form = MyUserChangeForm
admin.site.unregister(Group)
admin.site.register(Group, MyGroupAdmin)
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
If you are creating your own user management backend and only want to show your custom permissions you can filter out the default permissions by excluding permission with a name that starts with "Can".
WARNING:
You must remember not to name your permissions starting with "Can"!!!!
If they decide to change the naming convention this might not work.
With credit to pmdarrow this is how I did this in my project:
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import Permission
from django.contrib import admin
class UserEditForm(UserChangeForm):
class Meta:
model = User
exclude = (
'last_login',
'is_superuser',
'is_staff',
'date_joined',
)
user_permissions = forms.ModelMultipleChoiceField(
Permission.objects.exclude(name__startswith='Can'),
widget=admin.widgets.FilteredSelectMultiple(_('permissions'), False))
If you want to prevent Django from creating permissions, you can block the signals from being sent.
If you put this into a management/init.py in any app, it will bind to the signal handler before the auth framework has a chance (taking advantage of the dispatch_uid debouncing).
from django.db.models import signals
def do_nothing(*args, **kwargs):
pass
signals.post_syncdb.connect(do_nothing, dispatch_uid="django.contrib.auth.management.create_permissions")
signals.post_syncdb.connect(do_nothing, dispatch_uid="django.contrib.auth.management.create_superuser")
Related
Please consider a simple Django app containing a central model called Project. Other resources of this app are always tied to a specific Project.
Exemplary code:
class Project(models.Model):
pass
class Page(models.Model):
project = models.ForeignKey(Project)
I'd like to leverage Django's permission system to set granular permissions per existing project. In the example's case, a user should be able to have a view_page permission for some project instances, and don't have it for others.
In the end, I would like to have a function like has_perm that takes the permission codename and a project as input and returns True if the current user has the permission in the given project.
Is there a way to extend or replace Django's authorization system to achieve something like this?
I could extend the user's Group model to include a link to Project and check both, the group's project and its permissions. But that's not elegant and doesn't allow for assigning permissions to single users.
Somewhat related questions on the Django forum can be found here:
Authorization on sets of resources
How are you handling user permissions in more complex projects?
Related StackOverflow questions:
Django permissions via related objects permissions
Orignal answer: Use django-guardian
Edit.
As discussed in the comments, I think the django-guardian offers the easiest and cleanest way to achieve this. However, another solution is to create a custom user.
Create a custom user model. how-to
Override the has_perm method in your new user model.
from django.db import models
from my_app import Project
class CustomUser(...)
projects = models.ManyToManyField(Project)
def has_perm(self, perm, obj=None):
if isinstance(obj, Project):
if not obj in self.projects.objects.all():
return False
return super().has_perm(perm, obj)
I wasn't quite happy with the answers that were (thankfully!) proposed because they seemed to introduce overhead, either in complexity or maintenance. For django-guardian in particular I would have needed a way to keep those object-level permissions up-to-date while potentially suffering from (slight) performance loss. The same is true for dynamically creating permissions; I would have needed a way to keep those up-to-date and would deviate from the standard way of defining permissions (only) in the models.
But both answers actually encouraged me to take a more detailed look at Django's authentication and authorization system. That's when I realized that it's quite feasible to extend it to my needs (as it is so often with Django).
I solved this by introducing a new model, ProjectPermission, that links a Permission to a project and can be assigned to users and groups. This model represents the fact that a user or group has a permission for a specific project.
To utilize this model, I extended ModelBackend and introduced a parallel permission check, has_project_perm, that checks if a user has a permission for a specific project. The code is mostly analogous to the default path of has_perm as defined in ModelBackend.
By leveraging the default permission check, has_project_perm will return True if the user either has the project-specific permission or has the permission in the old-fashioned way (that I termed "global"). Doing so allows me to assign permissions that are valid for all projects without stating them explicitly.
Lastly, I extended my custom user model to access the new permission check by introducing a new method, has_project_perm.
# models.py
from django.contrib import auth
from django.contrib.auth.models import AbstractUser, Group, Permission
from django.core.exceptions import PermissionDenied
from django.db import models
from showbase.users.models import User
class ProjectPermission(models.Model):
"""A permission that is valid for a specific project."""
project = models.ForeignKey(Project, on_delete=models.CASCADE)
base_permission = models.ForeignKey(
Permission, on_delete=models.CASCADE, related_name="project_permission"
)
users = models.ManyToManyField(User, related_name="user_project_permissions")
groups = models.ManyToManyField(Group, related_name="project_permissions")
class Meta:
indexes = [models.Index(fields=["project", "base_permission"])]
unique_together = ["project", "base_permission"]
def _user_has_project_perm(user, perm, project):
"""
A backend can raise `PermissionDenied` to short-circuit permission checking.
"""
for backend in auth.get_backends():
if not hasattr(backend, "has_project_perm"):
continue
try:
if backend.has_project_perm(user, perm, project):
return True
except PermissionDenied:
return False
return False
class User(AbstractUser):
def has_project_perm(self, perm, project):
"""Return True if the user has the specified permission in a project."""
# Active superusers have all permissions.
if self.is_active and self.is_superuser:
return True
# Otherwise we need to check the backends.
return _user_has_project_perm(self, perm, project)
# auth_backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission
class ProjectBackend(ModelBackend):
"""A backend that understands project-specific authorization."""
def _get_user_project_permissions(self, user_obj, project):
return Permission.objects.filter(
project_permission__users=user_obj, project_permission__project=project
)
def _get_group_project_permissions(self, user_obj, project):
user_groups_field = get_user_model()._meta.get_field("groups")
user_groups_query = (
"project_permission__groups__%s" % user_groups_field.related_query_name()
)
return Permission.objects.filter(
**{user_groups_query: user_obj}, project_permission__project=project
)
def _get_project_permissions(self, user_obj, project, from_name):
if not user_obj.is_active or user_obj.is_anonymous:
return set()
perm_cache_name = f"_{from_name}_project_{project.pk}_perm_cache"
if not hasattr(user_obj, perm_cache_name):
if user_obj.is_superuser:
perms = Permission.objects.all()
else:
perms = getattr(self, "_get_%s_project_permissions" % from_name)(
user_obj, project
)
perms = perms.values_list("content_type__app_label", "codename").order_by()
setattr(
user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms}
)
return getattr(user_obj, perm_cache_name)
def get_user_project_permissions(self, user_obj, project):
return self._get_project_permissions(user_obj, project, "user")
def get_group_project_permissions(self, user_obj, project):
return self._get_project_permissions(user_obj, project, "group")
def get_all_project_permissions(self, user_obj, project):
return {
*self.get_user_project_permissions(user_obj, project),
*self.get_group_project_permissions(user_obj, project),
*self.get_user_permissions(user_obj),
*self.get_group_permissions(user_obj),
}
def has_project_perm(self, user_obj, perm, project):
return perm in self.get_all_project_permissions(user_obj, project)
# settings.py
AUTHENTICATION_BACKENDS = ["django_project.projects.auth_backends.ProjectBackend"]
My answer is on the basis of a user should be able to have a view_page permission for one project instance, and don't have it for another instance.
So basically you will have to catch first user visit == first model instance , you can create FirstVisit model which will catch and save each first instance using url, user.id and page.id, then you check if it exists.
# model
class Project(models.Model):
pass
class Page(models.Model):
project = models.ForeignKey(Project)
class FirstVisit(models.Model):
url = models.URLField()
user = models.ForeignKey(User)
page = models.ForeignKey(Page)
#views.py
def my_view(request):
if not FisrtVisit.objects.filter(user=request.user.id, url=request.path, page=request.page.id).exists():
# first time visit == first instance
#your code...
FisrtVisit(user=request.user, url=request.path, page=request.page.id).save()
based on this solution
I suggest to use device (computer or Smartphone) Mac Address instead of url using getmac for maximum first visit check
I have setup my Django (1.8) admin to allow superusers to create new users interactively. My User model is customized using AbstractUser which means my admin file looks like this:
admin.py
from django.contrib import admin
from app.models import CPRUser
class UserAdmin(admin.ModelAdmin):
model = CPRUser
extra = 1
admin.site.register(CPRUser, UserAdmin)
and here is the model:
class CPRUser(AbstractUser):
student = models.PositiveIntegerField(verbose_name=_("student"),
default=0,
blank=True)
saved = models.IntegerField(default=0)
This appears to work OK, I can go through the admin and set the password, username and all the other custom fields of a new user. However, when I try and login with the newly created user, some part of the authentication process fails. I login from a page which is using the auth_views.login view and the boilerplate django login template.
On the other hand, if I create a new user using either manage.py createsuperuser or createuser() within the django shell, these users can login fine. This leads me to suspect it is to do with password storage or hashing - currently in the admin I can just type in a new user's password. Thing is, that is what I want to be able to do. How can I get this desired result - I want non-IT savy managers (whose details I won't have) to be able to easily create new users in the admin. I am aware of the risks of such a system.
The docs seem contradictory on setting this interactive user creation up in one section:
"The “Add user” admin page is different than standard admin pages in that it requires you to choose a username and password before allowing you to edit the rest of the user’s fields."
and then a couple of paragraphs later:
"User passwords are not displayed in the admin (nor stored in the database)"
Here is a screen shot of my admin:
How can I make Django accept the login attempts of users created interactively via the admin?
This is described in the documentation,
If your custom User model extends django.contrib.auth.models.AbstractUser, you can use Django’s existing django.contrib.auth.admin.UserAdmin class.
So, extending UserAdmin should do the trick.
In Django-admin, Is it possible to make feature so that admin can create/edit/delete certain permission through django-admin while the server is running?
In django-admin I want the permission can be listed, with edit create and delete feature
Using permission in Meta subclass of models class will create custom permission through migrate script.
taken from https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#custom-permissions
class Task(models.Model):
...
class Meta:
permissions = (
("view_task", "Can see available tasks"),
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
)
This will insert values on auth_permission (and django_content_type)
But that requires database migrations, which means unlikely be done by user (Admin) but by the developer instead. And I believe it is necessary for application to be able to manage permission, creating, editing, deleting while the server is running.
So what is the best practice to make this functionality with Django? Should I keep using migrate & create them all in each model and to reload server everytime we implement new permission or feature in general? thankyou
You can register the Permission model to admin view:
from django.contrib.auth.models import Permission
from django.contrib import admin
admin.site.register(Permission)
The code can be anywhere where it gets executed, but admin.py of your application could be an intuitive place to stick it in. After this, you will be able to view, edit and remove the permissions.
You also need to select_related because it will result in a bunch of SQL queries
from django.contrib import admin
from django.contrib.auth.models import Permission
#admin.register(Permission)
class PermissionAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('content_type')
Last Month i posted question on stackoverflow and on Django-Users group on G+ and on django website too. But i didn't find any answer that can solve my problem. What i want to do is to add new permission named as view in django admin panel, so user can only view data!. I also followed different patches from django website and tried django-databrowse but nothing works as expected. I then finally decide to edit views of auth/admin. Now what i am going to do is to add view permission like:
1. Added 'view' to default permission list
#./contrib/auth/management/init.py
def _get_all_permissions(opts):
"Returns (codename, name) for all permissions in the given opts."
perms = []
for action in ('add', 'change', 'delete', 'view'):
perms.append((_get_permission_codename(action, opts), u'Can %s %s' % (action, opts.verbose_name_raw)))
return perms + list(opts.permissions)
2. Test the 'view' permission is added to all models
run manage.py syncdb
After this i can assign only view permission to user. Now this view permission must work too. So i am writing this code: in view.py of django-admin
for per in request.user.user_permissions_all():
print per
This code prints permissions assigned to login user like auth | permission | can view department etc
Now i can get permission type and model name by splitting this sentence. I will get all the model name of application and will match that which data must b visible. This is again not what i really need but can work.
So my question is :
* Is this is what i should do or is there any other way too. I just want a solution that must works as expected. Need Your Assistance *
Adding 'view' permission to default permissions list
Your solution works, but you should really avoid editing source code if possible. There's a few ways to accomplish this within the framework:
1. Add the permission during post_syncdb():
In a file under your_app/management/
from django.db.models.signals import post_syncdb
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission
def add_view_permissions(sender, **kwargs):
"""
This syncdb hooks takes care of adding a view permission too all our
content types.
"""
# for each of our content types
for content_type in ContentType.objects.all():
# build our permission slug
codename = "view_%s" % content_type.model
# if it doesn't exist..
if not Permission.objects.filter(content_type=content_type, codename=codename):
# add it
Permission.objects.create(content_type=content_type,
codename=codename,
name="Can view %s" % content_type.name)
print "Added view permission for %s" % content_type.name
# check for all our view permissions after a syncdb
post_syncdb.connect(add_view_permissions)
Whenever you issue a 'syncdb' command, all content types can be
checked to see if they have a 'view' permission, and if not, create
one.
SOURCE: The Nyaruka Blog
2. Add the permission to the Meta permissions option:
Under every model you would add something like this to its Meta options:
class Pizza(models.Model):
cheesiness = models.IntegerField()
class Meta:
permissions = (
('view_pizza', 'Can view pizza'),
)
This will accomplish the same as 1 except you have to manually add it to each class.
3. NEW in Django 1.7, Add the permission to the Meta default_permissions option:
In Django 1.7 they added the default_permissions Meta option. Under every model you would add 'view' to the default_permissions option:
class Pizza(models.Model):
cheesiness = models.IntegerField()
class Meta:
default_permissions = ('add', 'change', 'delete', 'view')
Test the 'view' permission is added to all models
As for testing the whether a user has the permission, you can test on the has_perm() function. For example:
user.has_perm('appname.view_pizza') # returns True if user 'Can view pizza'
The permission/authentication documentation for Django 1.4 provides the following snippet for creating custom permissions programmatically:
Edit: (I would like to employ this for permissions that are not necessarily linked to a specific model class, but more general permissions that span multiple types.)
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
content_type = ContentType.objects.get(app_label='myapp', model='BlogPost')
permission = Permission.objects.create(codename='can_publish',
name='Can Publish Posts',
content_type=content_type)
Source
My question is where this code should be placed. Obviously these should only be created once, but I don't want to have to do it in the shell. It seems like this should be stored in a file somewhere. (For documentation sake.)
Usually it is enough to just add the needed permissions to the corresponding model class using the permissions meta attribute.
This is from the official documentation:
class Task(models.Model):
...
class Meta:
permissions = (
("view_task", "Can see available tasks"),
("change_task_status", "Can change the status of tasks"),
("close_task", "Can remove a task by setting its status as closed"),
)