Adding specific permission to users with different roles in django - python

I am new to django and I am a bit confused on how the permission works, or if that is what I am supposed to use in my case.
So, I have my user/model:
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
ROLE_CHOICES = (
(0, ('Student')),
(1, ('Proffesor')),
(2, ('Administration'))
)
role = models.IntegerField(choices=ROLE_CHOICES, default=2)
And then I have my views in election/views.py:
class MainPage(View)
class ElectionList(LoginRequiredMixin, View)
class ElectionDetail(LoginRequiredMixin, View)
#only administration can create elections
class CreateElection(LoginRequiredMixin, CreateView)
How can I restrict a simple user (student, for example) to create an election?

Django already has a Permission and Group models and per-group permissions, so the cleanest "django compatible" way here would be to define "Students", "Professors" and "Administrators" as groups, setup their permissions (eventually adding custom ones if needed), add your users to the appropriate group(s), and test if your current user has the needed permissions for a given action using the permission_required decorator or since you're using a class-based view the PermissionRequiredMixin.
As a side note: since you're using ints for your role values, you may want to add pseudo_constants for them in your model:
class User(AbstractUser):
ROLE_STUDENT = 0
ROLE_PROFESSOR = 1
ROLE_ADMINISTRATOR = 2
ROLE_CHOICES = (
(ROLE_STUDENT, 'Student'),
(ROLE_PROFESSOR, 'Professor'),
(ROLE_ADMINISTRATOR, 'Administration')
)
So you can query / filter your model using sensible human-readable values instead of magic numbers, ie:
students = User.objects.filter(role=User.ROLE_STUDENT)
instead of
students = User.objects.filter(role=0)
But if you use contrib.auth.models.Group for permissions you may not even need this field at all, as you can get your queryset from the groups members.

You can use UserPassesTestMixin
eg.,
class LoginAndPermission(LoginRequiredMixin, UserPassesTestMixin):
def test_func(self):
return self.request.user.is_student
def get_login_url(self):
if self.request.user.is_authenticated():
# User is logged in but does not have permission
return "/permission-denied-url/"
else:
# User is not logged in
return "/login/"
class ElectionDetail(LoginAndPermission, View):

My solution could be an alternative of Django's Decorator.
I'm pretty interesting by your question.
When I have function in my views and I don't want to display this one to a user group, I have a templatetags file :
from django import template
from django.contrib.auth.models import Group
register = template.Library()
#register.filter(name='has_group')
def has_group(user, group_name):
group = Group.objects.get(name=group_name)
return group in user.groups.all()
Then, in my HTML file :
{% if request.user|has_group:"admin" %}
<li>Some part</li>
{% endif %}
I think it's possible in my templatetags to user permission directly in my views.py file, but I don't know How to do that.
Anyway, my method works very well up to now ;)

Read the Permission and Authorization documentation in https://docs.djangoproject.com/en/1.11/topics/auth/default

from django.contrib.auth.mixins import AccessMixin
class AddElectionPermission(AccessMixin):
raise_exception = True
permission_denied_message = 'permission deny'
def dispatch(self, request, *args, **kwargs):
if request.user.role != 0:
return self.handle_no_permission()
return super(AddElectionPermission, self).dispatch(request, *args, **kwargs)
#only administration can create elections
class CreateElection(LoginRequiredMixin, AddElectionPermission, CreateView)

Related

How to change permission based on request.query_params in Django rest frameworks?

I merged together two views that almost does the same thing except the permission, I want to change permission based on: if company id is in the params. If not, it would use a simple IsAuthenticated class and also created a permission for IsCompany.
class FilesView(ListAPIView):
serializer_class = FileSerializer
permission_classes = (IsAuthenticated,)
...
def get_queryset(self):
if 'company' in self.request.query_params:
# In this case I want the IsCompany Permission class
return get_company_files(self)
# Otherwise the regular one
return get_personal_files(self)
See custom permissions at DRF documentations.
A little example:
from rest_framework import permissions
class IsCompanyRequestBasedPermission(permissions.BasePermission):
message = '<description of your permission>'
def has_permission(self, request, view):
if 'company' in self.request.query_params:
# Make your decision.
And then add it to permission_classes. It would work just as you expect.
If you use
class FilesView(ModelViewSet):
instead of
class FilesView(ListAPIView)
you can use get_serializer_class method which could help you
For example
def get_serializer_class(self):
if "your statement":
return "FirstSerializer"
if "your statement":
return "SecondSerializer"

How to setup Django permissions to be specific to a certain model's instances?

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

Django: how to add users to group in bulk in admin panel?

I want to add many users to group based on filter and my selection in admin panel.
But the user change list view does not provide such action.
So is there a way to do this? Any snippet or app?
No shell please because I hope people other than developers can do it.
I am surprised Django does not provide a way for such a common task.
Thanks!
This can save some time but not flexible. I can't filter and really "bulk" add.
I'm afraid this particular functionality is not yet exposed on the panel. You'll have to roll your sleeves and open up the shell. You can use the following snippet, just put your filters where .get() and .exclude() are defined.
$ python manage.py shell
>>> from django.contrib.auth.models import User, Group
>>> my_group = Group.objects.get(name='groupname')
>>> my_users = User.objects.filter(<your_filter>)
>>> for my_user in my_users:
... my_group.user_set.add(my_user)
Create A new Form Like This to add users to group in bulk
from django.forms import ModelForm
from django import forms
from django.contrib.auth.models import Group, User
class GroupAdminForm(ModelForm):
class Meta:
model = Group
group_users = forms.ModelMultipleChoiceField(label=u'Usuários deste Grupo', queryset=User.objects.all())
def __init__(self, *args, **kwargs):
super(GroupAdminForm, self).__init__(*args, **kwargs)
users_id = list()
# Avoid 'No exception supplied'
try:
users = self.instance.group.user_set.all()
for u in users:
users_id.append(u.id)
if self.fields.has_key('group_users'):
self.fields['group_users'].initial = users_id
except Exception, e:
pass
def clean(self):
group = self.cleaned_data['group']
group.save()
if group.user_set.all():
group.user_set.clear()
try:
users = self.cleaned_data['group_users']
for user in users:
group.user_set.add(user)
except:
return self.cleaned_data
return self.cleaned_data

Django Admin - how to prevent deletion of some of the inlines

I have 2 models - for example, Book and Page.
Page has a foreign key to Book.
Each page can be marked as "was_read" (boolean), and I want to prevent deleting pages that were read (in the admin).
In the admin - Page is an inline within Book (I don't want Page to be a standalone model in the admin).
My problem - how can I achieve the behavior that a page that was read won't be deleted?
I'm using Django 1.4 and I tried several options:
Override "delete" to throw a ValidationError - the problem is that the admin doesn't "catch" the ValidationError on delete and you get an error page, so this is not a good option.
Override in the PageAdminInline the method - has_delete_permission - the problem here -it's per type so either I allow to delete all pages or I don't.
Are there any other good options without overriding the html code?
Thanks,
Li
The solution is as follows (no HTML code is required):
In admin file, define the following:
from django.forms.models import BaseInlineFormSet
class PageFormSet(BaseInlineFormSet):
def clean(self):
super(PageFormSet, self).clean()
for form in self.forms:
if not hasattr(form, 'cleaned_data'):
continue
data = form.cleaned_data
curr_instance = form.instance
was_read = curr_instance.was_read
if (data.get('DELETE') and was_read):
raise ValidationError('Error')
class PageInline(admin.TabularInline):
model = Page
formset = PageFormSet
You could disable the delete checkbox UI-wise by creating your own custom
formset for the inline model, and set can_delete to False there. For
example:
from django.forms import models
from django.contrib import admin
class MyInline(models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyInline, self).__init__(*args, **kwargs)
self.can_delete = False
class InlineOptions(admin.StackedInline):
model = InlineModel
formset = MyInline
class MainOptions(admin.ModelAdmin):
model = MainModel
inlines = [InlineOptions]
Another technique is to disable the DELETE checkbox.
This solution has the benefit of giving visual feedback to the user because she will see a grayed-out checkbox.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
def add_fields(self, form, index):
super().add_fields(form, index)
if some_criteria_to_prevent_deletion:
form.fields['DELETE'].disabled = True
This code leverages the Field.disabled property added in Django 1.9. As the documentation says, "even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data," so you don't need to add more code to prevent deletion.
In your inline, you can add the flag can_delete=False
EG:
class MyInline(admin.TabularInline):
model = models.mymodel
can_delete = False
I found a very easy solution to quietly avoid unwanted deletion of some inlines. You can just override delete_forms property method.
This works not just on admin, but on regular inlines too.
from django.forms.models import BaseInlineFormSet
class MyInlineFormSet(BaseInlineFormSet):
#property
def deleted_forms(self):
deleted_forms = super(MyInlineFormSet, self).deleted_forms
for i, form in enumerate(deleted_forms):
# Use form.instance to access object instance if needed
if some_criteria_to_prevent_deletion:
deleted_forms.pop(i)
return deleted_forms

How do i create a custom django backend for django-registration?

I've had a read of this
http://docs.b-list.org/django-registration/0.8/backend-api.html
and i've had a shot at making my own backend. I am doing this because I want to create a backend that disallows having the same email used for registration, and I wanted to change the email-error message. I also wanted to add in my own field!
Here is what I came up with:
from django import forms
from registration.forms import RegistrationForm
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from registration.forms import attrs_dict
class customRegistrationForm(RegistrationForm):
email2 = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict,
maxlength=75)),
label=_("Confirm email"))
def clean_email(self):
"""
Validate that the email is alphanumeric and is not already
in use.
"""
try:
email = User.objects.get(email__iexact=self.cleaned_data['email'])
except User.DoesNotExist:
return self.cleaned_data['email']
raise forms.ValidationError(_("That email already exists - if you have forgotten your password, go to the login screen and then select \"forgot password\""))
def clean(self):
"""
Verifiy that the values entered into the two email fields
match. Note that an error here will end up in
``non_field_errors()`` because it doesn't apply to a single
field.
"""
if 'email' in self.cleaned_data and 'email2' in self.cleaned_data:
if self.cleaned_data['email'] != self.cleaned_data['email2']:
raise forms.ValidationError(_("The two email fields didn't match."))
return super(RegistrationForm,clean)
The above goes in my init.py file (whetever that is)
Then, I have in my urls.py code:
url(r'^accounts/register/$',
register,
{ 'backend': 'myapp.forms.customRegistrationForm' },
name='registration_register'),
... #other urls here!
Now, when i go to the /accounts/register page, I get the following error:
AttributeError at /accounts/register/
'customRegistrationForm' object has no attribute 'registration_allowed'
Which is weird. It seems to be telling me I need a "registration_allowed" method added to my subclass. BUT, the subclass is a subclass of the RegistrationForm, which works fine and doesn't have that stuff defined... I know I can add these members in, but it seems to beat the purpose of extension, right?
UPDATE
Here is the code now that it works!
I broke up the varying classes into varying init.py files in different folders - one called "forms" and one called "backends", both of which sit in a folder "djangoRegistration" under my main project.
/forms/init.py
from django import forms
from registration.forms import RegistrationForm
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from registration.forms import attrs_dict
class customRegistrationForm(RegistrationForm):
def __init__(self, *args, **kw):
super(RegistrationForm, self).__init__(*args, **kw)
self.fields.keyOrder = [
'username',
'email',
'email2',
'password1',
'password2'
]
email2 = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict,
maxlength=75)),
label=_("Confirm email"))
def clean_email(self):
"""
Validate that the email is alphanumeric and is not already
in use.
"""
try:
email = User.objects.get(email__iexact=self.cleaned_data['email'])
except User.DoesNotExist:
return self.cleaned_data['email']
raise forms.ValidationError(_("That email already exists - if you have forgotten your password, go to the login screen and then select \"forgot password\""))
def clean(self):
"""
Verifiy that the values entered into the two email fields
match. Note that an error here will end up in
``non_field_errors()`` because it doesn't apply to a single
field.
"""
if 'email' in self.cleaned_data and 'email2' in self.cleaned_data:
if self.cleaned_data['email'] != self.cleaned_data['email2']:
raise forms.ValidationError(_("The two email fields didn't match."))
return super(RegistrationForm,clean)
/backends/init.py
from registration.backends.default import DefaultBackend
from dumpstownapp.djangoRegistration.forms import customRegistrationForm
class customDefaultBackend(DefaultBackend):
def get_form_class(self, request):
"""
Return the default form class used for user registration.
"""
return customRegistrationForm
and finally, my urls.py just references the new backend:
url(r'^accounts/register/$',
register,
{ 'backend': 'myapp.djangoRegistration.backends.customDefaultBackend' },
name='registration_register'),
#more urls here! yay!
As a final note, I had to add some code to "order" the way the fields were presented, which is what the init method in the customRegistrationForm is doing
thanks!
You're trying to use a form as a backend, but that's not what a backend is at all. As the document you link to explains, a backend is a class that implements certain methods, including registration_allowed. The form doesn't implement any of those, which is not surprising, because it's meant for user input and validation, not the backend actions.
However, that page does give a hint as to the correct way to implement this. One of the methods that the backend can define is get_form_class(), which returns the form class to use. So, it seems that what you need is a custom backend that inherits from registration.backends.default.DefaultBackend and overrides just the get_form_class method, which simply returns customRegistrationForm.

Categories

Resources