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'
Related
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.
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'm using Django rest framework, and my model is like this, Every Act can have more than one post.
class Act(models.Model):
user = models.ForeignKey("common.MyUser", related_name="act_user")
act_title = models.CharField(max_length=30)
act_content = models.CharField(max_length=1000)
act_type = models.IntField()
class Post(models.Model):
user = models.ForeignKey("common.MyUser", related_name="post_user")
act = models.ForeignKey("activities.Act", related_name="post_act")
post_title = models.CharField(max_length=30, blank=True)
post_content = models.CharField(max_length=140)
my view.py in DRF:
class PostList(generics.ListCreateAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def perform_create(self, serializer): #self is a instance of class or is a class here?
serializer.save(user=self.request.user)
This works fine, but what I want now is if act_type = 1 means this is a private Act and only the act author can create post under this act.I wonder how to use different permission_classes depend on different Act.Maybe looks like:
class PostList(generics.ListCreateAPIView):
if self.act_type == 1:
permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsActCreatorOrReadOnly)
else
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def perform_create(self, serializer): #self is a instance of class or is a class here?
serializer.save(user=self.request.user)
And I also want to know how to write this permissions.py:
class IsActCreatorOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.act.user == request.user
I don't know what obj really means here, and the error tell me obj.act doesn't exist.
EDIT
Here is my postSerializer.
class PostAllSerializer(serializers.ModelSerializer):
"""Posts api fields"""
post_user = UserSerializer(source="user", read_only=True)
post_author = serializers.ReadOnlyField(source='user.user_name')
class Meta:
model = Post
fields = ("id", "act", "post_author", "post_title", "post_content",)
I tried this, but not working, I still can create the post even I'm not the author of the Act(but the act_id is wrong):
def create(self, request, *args, **kwargs):
act_type = request.data.get("act_type")
if act_type == 0:
act_id = request.data.get("act")
act = Act.objects.get(pk=act_id)
if request.user != act.user:
return Response(status=403)
return super().create(request)
For using different permission classes, there is the get_permissions method that you can overwrite on your PostList view:
def get_permissions(self):
if self.request.method == 'POST':
return (OnePermission(),)
elif # other condition if you have:
return (AnotherPermission(),)
return (YetAnotherPermission(),)
However, in your case you can't use object level permissions, because you don't have an object instance yet. From the DRF docs (highlights by me):
REST framework permissions also support object-level permissioning. Object level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.
Object level permissions are run by REST framework's generic views when .get_object() is called.
When doing a POST request, you don't have any object yet, thus the object level permissions won't be invoked.
One way you could achieve what you want is by checking it in the create method of PostList view. Something like this (hypothetical code):
class PostList(generics.ListCreateAPIView):
...
def create(self, request):
act_id = request.data.get('act') # depending on your PostSerializer, the logic of getting act id can vary a little
act = Act.objects.get(pk=act_id) # assuming act always exists, otherwise account for in-existing act
if act.user != request.user:
return Response({details: "You shall not pass!!!", status=200) # change to a status and message you need here
# logic of Post creation here
Good luck!
When fields need to be filled programmatically in Django Rest Framework, the pre_save method may be overridden in the APIView, and the needed fields can be populated there, like:
def pre_save(self, obj):
obj.owner = self.request.user
This works great for flat objects, but in case of nested situations, the nested object cannot be accessed in the pre_save method. The only solution I found so far is to override the save_object method, and check if the object is an instance of the nested class, and if so, populate that field there. Although this works, I don't like the solution, and would like to know if anyone found a better way?
Demonstrating the situation:
class Notebook(models.Model):
owner = models.ForeignKey(User)
class Note(models.Model):
owner = models.ForeignKey(User)
notebook = models.ForeignKey(Notebook)
note = models.TextField()
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
class Meta:
model = Note
fields = ('note', 'owner')
class NotebookSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True)
owner = serializers.Field(source='owner.username')
class Meta:
model = Notebook
fields = ('notes', 'owner')
def save_object(self, obj, **kwargs):
if isinstance(obj, Note):
obj.owner = obj.notebook.owner
return super(NotebookSerializer, self).save_object(obj, **kwargs)
class NotebookCreateAPIView(CreateAPIView):
model = Notebook
permission_classes = (IsAuthenticated,)
serializer_class = NotebookSerializer
def pre_save(self, obj):
obj.owner = self.request.user
Before asking why don't I use different endpoints for creating notebooks and notes separately, let me say that I do that, but I also need a functionality to provide initial notes on creation of the notebook, so that's why I need this kind of endpoint as well.
Also, before I figured out this hackish solution, I actually expected that I will have to override the save_object method of the NoteSerializer class itself, but it turned out in case of nested objects, it won't even be called, only the root object's save_objects method, for all the nested objects, but I guess it was a design decision.
So once again, is this solvable in a more idiomatic way?
You can access the request in your serializer context.
So my approach to this would be:
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
def restore_object(self, attrs, instance=None):
instance = super(NoteSerializer, self).restore_object(attrs, instance)
instance.owner = self.context['request'].user
return instance
class Meta:
model = Note
fields = ('note', 'owner')
And the same on the NotebookSerializer.
The Serializer context will be made available to all used serializers in the ViewSet.
class TaskManager(models.Manager):
def get_query_set(self):
return super(TaskManager, self).get_query_set().filter(Owner='jim')
class Task(models.Model):
Name = models.CharField('Title', max_length=200)
Notes = models.TextField('Description',max_length=2000, null=True)
project = models.ForeignKey(Project,null=True, blank=True)
Owner = models.CharField(max_length=100, choices=owner_set)
objects = TaskManager()
def __unicode__(self):
return self.Name
I have two models in my models.py, every Task has a project. But when i enter the Task page, it will list all the task by default. So i want it to list only the current user's Task by default. It is that the Task.Owner=current user.
Who can help me with this, thanks!
I have searched from the net and then get the solution:
I update the code i pasted just now above.
Thanks for all.
Why not do this!
Task.objects.filter(owner = current_user)
Update:
1) If you want to filter in you own custom template add this to your view code
2) If you are trying to customize the admin site do this,
from django.contrib import admin
from models import MyModel
class TaskAdmin(admin.ModelAdmin):
def queryset(self, request):
return super(TaskAdmin, self).queryset(request).filter(owner = request.user)
admin.site.register(Task, TaskAdmin)
In the ModelAdmin class you should add:
class TaskAdmin(...):
def queryset(self, request):
qs = super(TaskAdmin, self).queryset(request)
return qs.filter(Owner=request.user)
This will override the default queryset
more here:
https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.queryset
I just want to modify the default filter of a model, make the model list not all items without the modelAdmin, because the modelAdmin just provide a filter which need manual click, i want to filter the model by default automaticlly