Reestructuring permissions, now discarted django-guardian, and using custom permission system

remotes/origin/enhancement/email-actions
Jesús Espino 2013-03-30 11:56:50 +01:00
parent 4c060d3b4e
commit 3245393b55
9 changed files with 135 additions and 33 deletions

View File

@ -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)

View File

@ -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),

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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'),
) )

View File

@ -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']

View File

@ -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'),

View File

@ -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