Reestructuring permissions, now discarted django-guardian, and using custom permission system
parent
4c060d3b4e
commit
3245393b55
|
@ -1,7 +1,8 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
from greenmine.base.models import Role
|
from greenmine.base.models import Role, User
|
||||||
|
|
||||||
admin.site.unregister(Group)
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
@ -19,3 +20,4 @@ class RoleAdmin(admin.ModelAdmin):
|
||||||
db_field, request=request, **kwargs)
|
db_field, request=request, **kwargs)
|
||||||
|
|
||||||
admin.site.register(Role, RoleAdmin)
|
admin.site.register(Role, RoleAdmin)
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
|
|
|
@ -25,6 +25,7 @@ class ApiRoot(APIView):
|
||||||
'user-stories': reverse('user-story-list', request=request, format=format),
|
'user-stories': reverse('user-story-list', request=request, format=format),
|
||||||
'changes': reverse('change-list', request=request, format=format),
|
'changes': reverse('change-list', request=request, format=format),
|
||||||
'change-attachments': reverse('change-attachment-list', request=request, format=format),
|
'change-attachments': reverse('change-attachment-list', request=request, format=format),
|
||||||
|
'issues': reverse('issue-list', request=request, format=format),
|
||||||
'tasks': reverse('task-list', request=request, format=format),
|
'tasks': reverse('task-list', request=request, format=format),
|
||||||
'severities': reverse('severity-list', request=request, format=format),
|
'severities': reverse('severity-list', request=request, format=format),
|
||||||
'issue-status': reverse('issue-status-list', request=request, format=format),
|
'issue-status': reverse('issue-status-list', request=request, format=format),
|
||||||
|
|
|
@ -64,31 +64,3 @@ class Role(models.Model):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return unicode(self.name)
|
return unicode(self.name)
|
||||||
|
|
||||||
|
|
||||||
if not hasattr(Group, 'role'):
|
|
||||||
field = models.ForeignKey(Role, blank=False, null=False, related_name='groups')
|
|
||||||
field.contribute_to_class(Group, 'role')
|
|
||||||
|
|
||||||
|
|
||||||
if not hasattr(Group, 'project'):
|
|
||||||
field = models.ForeignKey(Project, blank=False, null=False, related_name='groups')
|
|
||||||
field.contribute_to_class(Group, 'project')
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Role)
|
|
||||||
def role_post_save(sender, instance, **kwargs):
|
|
||||||
"""
|
|
||||||
Recalculate projects groups
|
|
||||||
"""
|
|
||||||
from greenmine.base.services import RoleGroupsService
|
|
||||||
RoleGroupsService().replicate_role_on_all_projects(instance)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=Role.permissions.through)
|
|
||||||
def role_m2m_changed(sender, instance, **kwargs):
|
|
||||||
"""
|
|
||||||
Recalculate projects groups
|
|
||||||
"""
|
|
||||||
from greenmine.base.services import RoleGroupsService
|
|
||||||
RoleGroupsService().replicate_role_on_all_projects(instance)
|
|
||||||
|
|
|
@ -6,6 +6,12 @@ from greenmine.scrum import models
|
||||||
import reversion
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipInline(admin.TabularInline):
|
||||||
|
model = models.Membership
|
||||||
|
fields = ('user', 'project', 'role')
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class MilestoneInline(admin.TabularInline):
|
class MilestoneInline(admin.TabularInline):
|
||||||
model = models.Milestone
|
model = models.Milestone
|
||||||
fields = ('name', 'owner', 'estimated_start', 'estimated_finish', 'closed', 'disponibility', 'order')
|
fields = ('name', 'owner', 'estimated_start', 'estimated_finish', 'closed', 'disponibility', 'order')
|
||||||
|
@ -27,7 +33,7 @@ class UserStoryInline(admin.TabularInline):
|
||||||
|
|
||||||
class ProjectAdmin(reversion.VersionAdmin):
|
class ProjectAdmin(reversion.VersionAdmin):
|
||||||
list_display = ["name", "owner"]
|
list_display = ["name", "owner"]
|
||||||
inlines = [MilestoneInline, UserStoryInline]
|
inlines = [MembershipInline, MilestoneInline, UserStoryInline]
|
||||||
|
|
||||||
admin.site.register(models.Project, ProjectAdmin)
|
admin.site.register(models.Project, ProjectAdmin)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from rest_framework import generics
|
||||||
|
|
||||||
from greenmine.scrum.serializers import *
|
from greenmine.scrum.serializers import *
|
||||||
from greenmine.scrum.models import *
|
from greenmine.scrum.models import *
|
||||||
|
from greenmine.scrum.permissions import *
|
||||||
|
|
||||||
class SimpleFilterMixin(object):
|
class SimpleFilterMixin(object):
|
||||||
filter_fields = []
|
filter_fields = []
|
||||||
|
@ -36,10 +36,14 @@ class ProjectList(generics.ListCreateAPIView):
|
||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
|
class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
|
permission_classes = (ProjectDetailPermission,)
|
||||||
|
|
||||||
|
|
||||||
class MilestoneList(SimpleFilterMixin, generics.ListCreateAPIView):
|
class MilestoneList(SimpleFilterMixin, generics.ListCreateAPIView):
|
||||||
|
@ -47,10 +51,14 @@ class MilestoneList(SimpleFilterMixin, generics.ListCreateAPIView):
|
||||||
serializer_class = MilestoneSerializer
|
serializer_class = MilestoneSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class MilestoneDetail(generics.RetrieveUpdateDestroyAPIView):
|
class MilestoneDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Milestone
|
model = Milestone
|
||||||
serializer_class = MilestoneSerializer
|
serializer_class = MilestoneSerializer
|
||||||
|
permission_classes = (MilestoneDetailPermission,)
|
||||||
|
|
||||||
|
|
||||||
class UserStoryList(SimpleFilterMixin, generics.ListCreateAPIView):
|
class UserStoryList(SimpleFilterMixin, generics.ListCreateAPIView):
|
||||||
|
@ -58,6 +66,9 @@ class UserStoryList(SimpleFilterMixin, generics.ListCreateAPIView):
|
||||||
serializer_class = UserStorySerializer
|
serializer_class = UserStorySerializer
|
||||||
filter_fields = ('project', 'milestone')
|
filter_fields = ('project', 'milestone')
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = UserStory
|
model = UserStory
|
||||||
|
@ -68,6 +79,9 @@ class ChangeList(generics.ListCreateAPIView):
|
||||||
model = Change
|
model = Change
|
||||||
serializer_class = ChangeSerializer
|
serializer_class = ChangeSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class ChangeDetail(generics.RetrieveUpdateDestroyAPIView):
|
class ChangeDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Change
|
model = Change
|
||||||
|
@ -78,17 +92,37 @@ class ChangeAttachmentList(generics.ListCreateAPIView):
|
||||||
model = ChangeAttachment
|
model = ChangeAttachment
|
||||||
serializer_class = ChangeAttachmentSerializer
|
serializer_class = ChangeAttachmentSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(change__project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class ChangeAttachmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
class ChangeAttachmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = ChangeAttachment
|
model = ChangeAttachment
|
||||||
serializer_class = ChangeAttachmentSerializer
|
serializer_class = ChangeAttachmentSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class IssueList(generics.ListCreateAPIView):
|
||||||
|
model = Issue
|
||||||
|
serializer_class = IssueSerializer
|
||||||
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
model = Issue
|
||||||
|
serializer_class = IssueSerializer
|
||||||
|
|
||||||
|
|
||||||
class TaskList(generics.ListCreateAPIView):
|
class TaskList(generics.ListCreateAPIView):
|
||||||
model = Task
|
model = Task
|
||||||
serializer_class = TaskSerializer
|
serializer_class = TaskSerializer
|
||||||
filter_fields = ('user_story', 'milestone', 'project')
|
filter_fields = ('user_story', 'milestone', 'project')
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class TaskDetail(generics.RetrieveUpdateDestroyAPIView):
|
class TaskDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Task
|
model = Task
|
||||||
|
@ -100,6 +134,9 @@ class SeverityList(generics.ListCreateAPIView):
|
||||||
serializer_class = SeveritySerializer
|
serializer_class = SeveritySerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class SeverityDetail(generics.RetrieveUpdateDestroyAPIView):
|
class SeverityDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Severity
|
model = Severity
|
||||||
|
@ -111,6 +148,9 @@ class IssueStatusList(generics.ListCreateAPIView):
|
||||||
serializer_class = IssueStatusSerializer
|
serializer_class = IssueStatusSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class IssueStatusDetail(generics.RetrieveUpdateDestroyAPIView):
|
class IssueStatusDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = IssueStatus
|
model = IssueStatus
|
||||||
|
@ -122,6 +162,9 @@ class TaskStatusList(SimpleFilterMixin, generics.ListCreateAPIView):
|
||||||
serializer_class = TaskStatusSerializer
|
serializer_class = TaskStatusSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class TaskStatusDetail(generics.RetrieveUpdateDestroyAPIView):
|
class TaskStatusDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = TaskStatus
|
model = TaskStatus
|
||||||
|
@ -133,6 +176,9 @@ class UserStoryStatusList(generics.ListCreateAPIView):
|
||||||
serializer_class = UserStoryStatusSerializer
|
serializer_class = UserStoryStatusSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class UserStoryStatusDetail(generics.RetrieveUpdateDestroyAPIView):
|
class UserStoryStatusDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = UserStoryStatus
|
model = UserStoryStatus
|
||||||
|
@ -144,6 +190,9 @@ class PriorityList(generics.ListCreateAPIView):
|
||||||
serializer_class = PrioritySerializer
|
serializer_class = PrioritySerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class PriorityDetail(generics.RetrieveUpdateDestroyAPIView):
|
class PriorityDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Priority
|
model = Priority
|
||||||
|
@ -155,6 +204,9 @@ class IssueTypeList(generics.ListCreateAPIView):
|
||||||
serializer_class = IssueTypeSerializer
|
serializer_class = IssueTypeSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class IssueTypeDetail(generics.RetrieveUpdateDestroyAPIView):
|
class IssueTypeDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = IssueType
|
model = IssueType
|
||||||
|
@ -166,6 +218,9 @@ class PointsList(generics.ListCreateAPIView):
|
||||||
serializer_class = PointsSerializer
|
serializer_class = PointsSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class PointsDetail(generics.RetrieveUpdateDestroyAPIView):
|
class PointsDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Points
|
model = Points
|
||||||
|
|
|
@ -105,6 +105,14 @@ class Points(models.Model):
|
||||||
return u"project({0})/point({1})".format(self.project.id, self.name)
|
return u"project({0})/point({1})".format(self.project.id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(models.Model):
|
||||||
|
user = models.ForeignKey("base.User")
|
||||||
|
project = models.ForeignKey("Project")
|
||||||
|
role = models.ForeignKey("base.Role")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('user', 'project')
|
||||||
|
|
||||||
class Project(models.Model):
|
class Project(models.Model):
|
||||||
uuid = models.CharField(max_length=40, unique=True, blank=True)
|
uuid = models.CharField(max_length=40, unique=True, blank=True)
|
||||||
name = models.CharField(max_length=250, unique=True)
|
name = models.CharField(max_length=250, unique=True)
|
||||||
|
@ -114,7 +122,8 @@ class Project(models.Model):
|
||||||
created_date = models.DateTimeField(auto_now_add=True)
|
created_date = models.DateTimeField(auto_now_add=True)
|
||||||
modified_date = models.DateTimeField(auto_now_add=True, auto_now=True)
|
modified_date = models.DateTimeField(auto_now_add=True, auto_now=True)
|
||||||
|
|
||||||
owner = models.ForeignKey("base.User", related_name="projects")
|
owner = models.ForeignKey("base.User", related_name="owned_projects")
|
||||||
|
members = models.ManyToManyField("base.User", related_name="projects", through='Membership')
|
||||||
public = models.BooleanField(default=True)
|
public = models.BooleanField(default=True)
|
||||||
|
|
||||||
last_us_ref = models.BigIntegerField(null=True, default=1)
|
last_us_ref = models.BigIntegerField(null=True, default=1)
|
||||||
|
@ -170,6 +179,8 @@ class Project(models.Model):
|
||||||
|
|
||||||
('create_milestone', 'Can create milestones'),
|
('create_milestone', 'Can create milestones'),
|
||||||
('modify_milestone', 'Can modify milestones'),
|
('modify_milestone', 'Can modify milestones'),
|
||||||
|
('view_milestone', 'Can view milestones'),
|
||||||
|
('delete_milestone', 'Can delete milestones'),
|
||||||
|
|
||||||
('manage_users', 'Can manage users'),
|
('manage_users', 'Can manage users'),
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from greenmine.scrum.models import Membership
|
||||||
|
|
||||||
|
def has_project_perm(user, project, perm):
|
||||||
|
if user.is_authenticated():
|
||||||
|
try:
|
||||||
|
membership = Membership.objects.get(project=project, user=user)
|
||||||
|
if membership.role.permissions.filter(codename=perm).count() > 0:
|
||||||
|
return True
|
||||||
|
except Membership.DoesNotExist:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDetailPermission(permissions.BasePermission):
|
||||||
|
get_permission = None
|
||||||
|
put_permission = None
|
||||||
|
delete_permission = None
|
||||||
|
safe_methods = ['HEAD', 'OPTIONS']
|
||||||
|
path_to_project = []
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
if request.method in self.safe_methods:
|
||||||
|
return True
|
||||||
|
|
||||||
|
project_obj = obj
|
||||||
|
for attrib in self.path_to_project:
|
||||||
|
project_obj = getattr(project_obj, attrib)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
return has_project_perm(request.user, project_obj, self.get_permission)
|
||||||
|
|
||||||
|
elif request.method == "PUT":
|
||||||
|
return has_project_perm(request.user, project_obj, self.put_permission)
|
||||||
|
|
||||||
|
elif request.method == "DELETE":
|
||||||
|
return has_project_perm(request.user, project_obj, self.delete_permission)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
class ProjectDetailPermission(BaseDetailPermission):
|
||||||
|
get_permission = "view_projects"
|
||||||
|
put_permission = "modify_projects"
|
||||||
|
delete_permission = "delete_projects"
|
||||||
|
safe_methods = ['HEAD', 'OPTIONS']
|
||||||
|
path_to_project = []
|
||||||
|
|
||||||
|
class MilestoneDetailPermission(BaseDetailPermission):
|
||||||
|
get_permission = "view_milestone"
|
||||||
|
put_permission = "modify_milestone"
|
||||||
|
delete_permission = "delete_milestone"
|
||||||
|
safe_methods = ['HEAD', 'OPTIONS']
|
||||||
|
path_to_project = ['project']
|
|
@ -14,6 +14,8 @@ urlpatterns = format_suffix_patterns(patterns('',
|
||||||
url(r'^changes/(?P<pk>[0-9]+)/$', api.ChangeDetail.as_view(), name='change-detail'),
|
url(r'^changes/(?P<pk>[0-9]+)/$', api.ChangeDetail.as_view(), name='change-detail'),
|
||||||
url(r'^change_attachments/$', api.ChangeAttachmentList.as_view(), name='change-attachment-list'),
|
url(r'^change_attachments/$', api.ChangeAttachmentList.as_view(), name='change-attachment-list'),
|
||||||
url(r'^change_attachments/(?P<pk>[0-9]+)/$', api.ChangeAttachmentDetail.as_view(), name='change-attachment-detail'),
|
url(r'^change_attachments/(?P<pk>[0-9]+)/$', api.ChangeAttachmentDetail.as_view(), name='change-attachment-detail'),
|
||||||
|
url(r'^issues/$', api.IssueList.as_view(), name='issue-list'),
|
||||||
|
url(r'^issues/(?P<pk>[0-9]+)/$', api.IssueDetail.as_view(), name='issue-detail'),
|
||||||
url(r'^tasks/$', api.TaskList.as_view(), name='task-list'),
|
url(r'^tasks/$', api.TaskList.as_view(), name='task-list'),
|
||||||
url(r'^tasks/(?P<pk>[0-9]+)/$', api.TaskDetail.as_view(), name='task-detail'),
|
url(r'^tasks/(?P<pk>[0-9]+)/$', api.TaskDetail.as_view(), name='task-detail'),
|
||||||
url(r'^severities/$', api.SeverityList.as_view(), name='severity-list'),
|
url(r'^severities/$', api.SeverityList.as_view(), name='severity-list'),
|
||||||
|
|
|
@ -7,7 +7,6 @@ billiard==2.7.3.23
|
||||||
celery==3.0.17
|
celery==3.0.17
|
||||||
django-celery==3.0.11
|
django-celery==3.0.11
|
||||||
django-grappelli==2.4.4
|
django-grappelli==2.4.4
|
||||||
django-guardian==1.1.0.beta
|
|
||||||
django-reversion==1.7
|
django-reversion==1.7
|
||||||
git+git://github.com/toastdriven/django-haystack.git
|
git+git://github.com/toastdriven/django-haystack.git
|
||||||
django-picklefield==0.3.0
|
django-picklefield==0.3.0
|
||||||
|
|
Loading…
Reference in New Issue