I have a problem for block access to not authorized user in pages dedicated to add new objects. List of that users is stored in many-to-many field in project object and in foreign key field.
Below is models.py
class Project(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="projects_as_owner", null=True)
project_managers = models.ManyToManyField(User, related_name="projects_as_pm", blank=True)
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
date_of_insert = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Milestone(models.Model):
project_fk = models.ForeignKey(Project, related_name="milestones", on_delete=models.CASCADE)
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
date_of_insert = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
And views.py with class I have problem
class NewMilestone(LoginRequiredMixin, generic.CreateView):
model = Milestone
fields = ['name', 'description']
lookup_url_kwarg = 'p_id'
template_name = 'main/new_milestone.html'
# ... two functions, that work good, not important here ...
def get_queryset(self):
qs = super(NewMilestone, self).get_queryset()
project = Project.objects.get(id=self.kwargs['p_id'])
if(qs.filter(project_fk__owner=self.request.user).exists() or User.objects.filter(id=self.request.user.id).filter(projects_as_pm__id=project.id).exists()):
return qs
else:
return Http404("You are not authenticated to this action")
Objective here is here to allow authenticated users (owner and project manager/s) to enter this view and for anybody else show info about declined access.
Problem is that, that method, get_queryset, doesn't block unauthorised users in CreateViev class.
I tried some configurations for that issue, every single one I used had this flaw.
My question here is how to make it work the way I expect from it?
PS. English is not my native language and it was a while since I wrote something, so please be understanding.
You are using the LoginRequiredMixin which is a good thing. But then you didn't set any of the parameters available.
LoginRequiredMixin inherits from AccessMixin and you can use all it's parameters with which it shouldn't be too complicated to cover your case.
Here's a possible implementation:
class NewMilestone(LoginRequiredMixin, generic.CreateView):
...
# your class attributes
...
raise_exception = True
# Returns a permission denied message. Default: empty string
def get_permission_denied_message(self):
return "Access is restricted to authenticated users"
If you have raise_exception set to True then the get_permission_denied_message method will be called. Otherwise the user will be redirected to the login_url which you also would have to declare as a class attribute.
Related
Here is the scenario I am working on: I have django app that creates records which I call sessions:
blog.models.py
class Session(models.Model):
uid = models.CharField(max_length=50, blank=True)
cid = models.CharField(max_length=50, blank=True)
action_type = models.CharField(max_length=50, blank=True)
action_name = models.CharField(max_length=50, blank=True)
action_value = models.CharField(max_length=50, blank=True)
session_date = models.DateTimeField(auto_now_add=True)
client = models.CharField(max_length=50, blank=True)
I have a dashboard page to show charts and a database page to show the records as a table:
blog.urls.py
path('', auth_views.LoginView.as_view(template_name='users/login.html'), name='blog-home'),
path('<str:username>/dashboard/', views.dashboard and DashboardListView.as_view(), name='blog-dashboard'),
path('<str:username>/database/', views.database and SessionListView.as_view(), name='blog-database'),
So when you log in, my SessionListView.as_view() goes through the whole database and displays only those records where the Session.client == the url's 'username' value.
Example: when user: DummyCo logs in (www.website.com/DummyCo/database/) they see only Session records where the Session.client field is 'DummyCo.' This has worked out great so far.
But here is the problem: I now need to provide multiple logins to users to see the same dashboard and database page.
Example: jim#DummyCo.com and amy#DummyCo.com both need to see the DummyCo records, but if I provided them with their own logins then their username's in the url would not match and thus the DummyCo records would not show. I thought using the built-in django Groups would be a solution but that seems to only help with authentication and permissions on the backend. I also extended my user model with a Profile model:
users/models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT)
image = models.ImageField(default='default.jpg', upload_to='profile_pics')
user_client = models.CharField(max_length=50, blank=True, null=True, default=None)
def __str__(self):
return f'{self.user.username} Profile'
I made the user_client model field to try and connect the Profile (and thus User) with the Session.client field: instead of <str:username>/database/ I thought i'd be able to use <str:client_user>/database/ and simply fill that field with 'DummyCo' on both Jim and Amy's profile to give them access to the records.
I read in a couple of places that the key to handling this problem is to switch the user model from one-to-one to many-to-one type early or before i build out the app. Unfortunately I have already put a ton of work into this project. I also read that I should look at the built-in User model as more of an account and less of a user. So is there a simple way to give multiple users access to one User/account?
Also, here is the views:
blog/views.py
class SessionListView(LoginRequiredMixin, ListView):
model = Session, Profile
template_name = 'blog/database.html'
context_object_name = 'sessions'
ordering = ['-session_date']
paginate_by = 25
def get_queryset(self):
user = get_object_or_404(User, username=self.kwargs.get('username'))
return Session.objects.filter(client=user).order_by('-session_date')
def get_context_data(self, **kwargs):
user = get_object_or_404(User, username=self.kwargs.get('username'))
context = super().get_context_data(**kwargs)
context['distinct_campaigns'] = Session.objects.filter(client=user).values('cid').distinct().order_by('cid')
context['distinct_action_types'] = Session.objects.filter(client=user)\
.values('action_type')\
.distinct().order_by('action_type')
return context
# login_required()
def database(request):
context = {
'sessions': Session.objects.all()
}
return render(request, 'blog/database.html', context, {'title': 'Database'})
Okay I figured out a solution:
I thought I needed to do some trickery on the html file within the for loop showing my query set sessions but it turns out that can be adjusted in my views.py file. Before this update my views.py looked like this:
class SessionListView(LoginRequiredMixin, ListView):
model = Session, Profile
template_name = 'blog/database.html'
context_object_name = 'sessions'
ordering = ['-session_date']
paginate_by = 25
def get_queryset(self):
user = get_object_or_404(User, username=self.kwargs.get('username'))
return Session.objects.filter(client=user).order_by('-session_date')
def get_context_data(self, **kwargs):
user = get_object_or_404(User, username=self.kwargs.get('username'))
context = super().get_context_data(**kwargs)
context['distinct_campaigns'] = Session.objects.filter(client=user).values('cid').distinct().order_by('cid')
context['distinct_action_types'] = Session.objects.filter(client=user)\
.values('action_type')\
.distinct().order_by('action_type')
return context
I realized the def get_queryset(self) was grabbing the logged-in username, then reviewing the full database and adding all records with the same session.client value as the value of the logged in user (i.e. DummyCo). So to make this work for a user like 'DummyCo_Sally', I changed the logic in that def like so:
class SessionListView(LoginRequiredMixin, ListView):
# gets the actual user (i.e. DummyCo_Sally)
user = get_object_or_404(User, username=self.kwargs.get('username'))
# turns user to a string
user_string = str(user)
# designates the _ as the separator
sep = '_'
# strips off _ and everything after it
stripped_user = user_string.split(sep, 1)[0]
# establishes the queryset as 'DummyCo' even though 'DummyCo_sally' is logged in
return Session.objects.filter(client=stripped_user).order_by('-session_date')
I doubt this method is the best way of handling multiple users seeing one umbrella data set, but it did the trick for me. This method also likely creates a security risk for applications that have public-facing user registration. But it did the trick for me.
I am working in a company which uses machines and molds to produce industrial parts. During the production process machines can malfunction so we created a frontend and backend for workers to register those error records. Recently engineers and managers from another departments joined to this system so I needed to make sure who can do what in other word managing permissions.
Relevant model:
class FaultRecord(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
fault = models.ForeignKey(Fault, on_delete=models.PROTECT)
machine = models.ForeignKey(Machine, on_delete=models.PROTECT)
mold = models.ForeignKey(Mold, on_delete=models.PROTECT)
part = models.ForeignKey(Part, on_delete=models.PROTECT)
material = models.ForeignKey(Material, on_delete=models.PROTECT)
responsible_departments = models.ManyToManyField(Department)
status = models.ForeignKey(FaultRecordStatus, on_delete=models.SET_NULL, null=True)
reason = models.TextField()
temporary_action = models.TextField()
permanent_action = models.TextField(null=True)
duration = models.PositiveSmallIntegerField()
occured_at = models.DateTimeField()
created_at = models.DateTimeField()
updated_at = models.DateTimeField(null=True)
Relevant view:
class FaultRecordViewSet(ModelViewSet):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated, ModelPermission]
serializer_class = FaultRecordSerializer
queryset = FaultRecord.objects.prefetch_related(
'user',
'fault',
'machine',
'mold',
'part',
'material',
'responsible_departments',
'status'
).all().order_by('occured_at')
model = FaultRecord
As we know Django creates 4 default permission for models which are view, add, change and delete. Based on this I wanted to check user permissions by request method so I don't have to write permission class for every model. Also I don't want to use has_object_permission because I check post permission too.
Solution I found:
class ModelPermission(BasePermission):
method_mapper = {
'GET': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete'
}
def get_model_permission(self, method, model):
app_label = model._meta.app_label
model_name = model._meta.model_name
permission_name = self.method_mapper.get(method)
return f'{app_label}.{permission_name}_{model_name}'
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
permission = self.get_model_permission(request.method, view.model)
return request.user.has_perm(permission)
I added model attribute to view class so get_model_permission returns required string to be able to use has_perm for me. With this I can create groups with permissions and set user groups. I searched a lot but couldn't find anything usefull for me. What do you think? I need opinions from others.
You are going in right direction, but I would suggest using DjangoModelPermissions instead of BasePermission. DjangoModelPermissions implements the get_required_permissions and has_permission functions so, you need not write those functions yourself. If you don't want to check object permissions then assign DjangoModelPermissions in your viewset.
You are right about managing permissions with groups. Create groups and assign permissions to those groups and Then assign groups to users. You can also assign some permissions to a user directly if the situation demands. has_permission of DjangoModelPermissions by default check permission assigned to user as well as user groups.
I have an app for managing test cases, which are organised into various projects. I'm trying to set permissions on a per project basis, i.e. every user has different permissions for each project. Here's what I've come up with so far:
class TestProjectMember(models.Model):
"""Per project permissions - a role can be set for each user for each project"""
member_name = models.ForeignKey(User, on_delete=models.SET_NULL)
project = models.ForeignKey(TestProject, on_delete=models.CASCADE)
member_role = models.CharField(choices=Choices.roles)
class TestCase(models.Model):
"""Test cases"""
tc_title = models.CharField(max_length=500, unique=True)
tc_project = models.ForeignKey(TestProject, on_delete=models.CASCADE)
class TestProject(models.Model):
"""Projects"""
project_name = models.CharField(max_length=200)
project_desc = models.CharField(max_length=500)
class TestCaseEditHeader(View):
def get(self, request, pk):
case = get_object_or_404(TestCase, id=pk)
if self.get_perm(case.tc_project, request.user, 'TA'):
form = TestCaseHeaderForm(instance=case)
context = {'case': case, 'form': form}
return render(request, 'test_case/tc_header_edit.html', context)
else:
return redirect('case_list')
def get_perm(self, curr_project, curr_user, perm):
model_perm = TestProjectMember.objects.filter(member_name=curr_user,
project=curr_project).values_list('member_role', flat=True)
if perm in model_perm:
return True
return False
It works, but it's a bit clunky. I'd have to call the get_perm() method from every get() or post() method from each view. A better solution would probably be a mixin. What has me stumped is how to pass the required role to the mixin for each view. For each view there is a required role that the user has to have to be able to use the view for the project the test case belongs to. How do I tell the mixin, which particular role is required for which view?
You can just set it as a class attribute.
Note, your query is pretty inefficient; you should just request the perm you want directly.
class TestPermMixin:
def get_perm(self, curr_project, curr_user):
return TestProjectMember.objects.filter(
member_name=curr_user, project=curr_project, member_role=self.perm
).exists()
And then set the attribute in the concrete class:
class TestCaseEditHeader(View):
perm = 'TA'
I have made Custom User model in my Django project. Here it is:
class CustomUser(User):
avatar = models.ImageField(upload_to='avatars')
about_myself = models.TextField(max_length=300)
USERNAME_FIELD = 'username'
def __str__(self):
return self.username
def is_author(self):
return 'blog.change_post' and 'blog.add_post' in self.get_all_permissions()
And after it, I changed all Foreign Keys of user to new CustomUser model. It works OK. But I make one new migration and django cause error, when I want to migrate it:
ValueError: Lookup failed for model referenced by field blog.Comment.author: main.CustomUser
My blog.Comment model:
class Comment(models.Model):
content = models.TextField()
author = models.ForeignKey(CustomUser)
date_create = models.DateTimeField(auto_now_add=True)
post = models.ForeignKey(Post)
What should I do?
Thanks!
Judging from the code you posted, you might be might be better served by extending the user model rather than replacing it. This pattern is usually called a profile model and works via a one-to-one relationship with User.
Profiles provides application specific fields and behaviors, while allowing User to go about it's usual business unchanged. It doesn't require you to muck around with rewriting auth or even necessarily change your foreign keys.
Here's an example of your code written as a profile:
class Profile(models.Model):
# Link to user :
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to='avatars')
about_myself = models.TextField(max_length=300)
def __str__(self):
return self.user.username
def is_author(self):
return 'blog.change_post' and 'blog.add_post' in self.user.get_all_permissions()
Comment model:
class Comment(models.Model):
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL)
date_create = models.DateTimeField(auto_now_add=True)
post = models.ForeignKey(Post)
# How to access the profile:
def check_author(self):
self.author.profile.is_author()
You'll also want to add a signal to create a new profile when a user is registered:
#receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile_for_new_user(sender, created, instance, **kwargs):
if created:
profile = Profile(user=instance)
profile.save()
Django docs on extending users.
If a profile approach doesn't work for you, try inheriting from AbstractUser or AbstractBaseUser instead of User. The abstract models provide the same basic functionality as User and are the preferred technique for recent Django versions.
There are a handful of additional steps however, check out the docs on creating custom users for a run down.
My model:
class Document(models.Model):
title = models.CharField(_('title'), null=False, blank=False, max_length=250)
description = models.TextField(_('description'), null=True, blank=True)
is_favourite = my_method()
class FavouriteDocumentUser(models.Model):
document = models.ForeignKey(Document)
user = models.ForeignKey(CustomUser)
class Meta:
unique_together = ('document', 'user',)
I need a field 'is_favourite' that is true if exist in FavouriteDocumentUser a row with the id of the document and the id of logged user.
So the problem is: how can I get the current user in a method of the model?
I'm using these models into django rest framework.
You can't, because Django ORM is not aware of Django authentication. The current user is available in the request object, which is only available to your views. You need to pass your request.user to your model method. For example:
class Document(models.Model):
...
def is_favourite(user):
return self.favoritedocumentuser_set().filter(user=user).exists()
then you call this method from your views:
def my_view(request):
...
if mydocument.is_favorite(request.user):
...do something...