201 строка
7.2 KiB
Python
201 строка
7.2 KiB
Python
from functools import partial
|
|
|
|
from django.db.models import Q
|
|
from django.http import Http404
|
|
|
|
import commonware.log
|
|
from rest_framework.exceptions import PermissionDenied
|
|
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
|
|
ListModelMixin, RetrieveModelMixin)
|
|
from rest_framework.permissions import BasePermission
|
|
from rest_framework.relations import HyperlinkedRelatedField, RelatedField
|
|
from rest_framework.serializers import ModelSerializer
|
|
|
|
from access import acl
|
|
from comm.models import (CommunicationNote, CommunicationThread,
|
|
CommunicationThreadCC, CommunicationThreadToken)
|
|
from mkt.api.authentication import (RestOAuthAuthentication,
|
|
RestSharedSecretAuthentication)
|
|
from mkt.api.base import CORSViewSet
|
|
from mkt.webpay.forms import PrepareForm
|
|
|
|
log = commonware.log.getLogger('mkt.comm')
|
|
|
|
|
|
class NoteSerializer(ModelSerializer):
|
|
thread = HyperlinkedRelatedField(view_name='comm-thread-detail')
|
|
body = RelatedField('body')
|
|
|
|
class Meta:
|
|
model = CommunicationNote
|
|
fields = ('id', 'author', 'note_type', 'body', 'created', 'thread')
|
|
|
|
|
|
class ThreadSerializer(ModelSerializer):
|
|
notes = HyperlinkedRelatedField(read_only=True, many=True,
|
|
view_name='comm-note-detail')
|
|
|
|
class Meta:
|
|
model = CommunicationThread
|
|
fields = ('id', 'addon', 'version', 'notes', 'created')
|
|
view_name = 'comm-thread-detail'
|
|
|
|
|
|
class ThreadPermission(BasePermission):
|
|
"""
|
|
Permission wrapper for checking if the authenticated user has the
|
|
permission to view the thread.
|
|
"""
|
|
|
|
def check_acls(self, request, obj, acl_type):
|
|
if acl_type == 'moz_contact':
|
|
return request.user.email == obj.addon.mozilla_contact
|
|
elif acl_type == 'admin':
|
|
return acl.action_allowed(request, 'Admin', '%')
|
|
elif acl_type == 'reviewer':
|
|
return acl.action_allowed(request, 'Apps', 'Review')
|
|
elif acl_type == 'senior_reviewer':
|
|
return acl.action_allowed(request, 'Apps', 'ReviewEscalated')
|
|
else:
|
|
raise 'Invalid ACL lookup.'
|
|
|
|
def has_permission(self, request, view):
|
|
# Let `has_object_permission` handle the permissions when we retrieve
|
|
# an object.
|
|
if view.action == 'retrieve':
|
|
return True
|
|
if not request.user.is_authenticated():
|
|
raise PermissionDenied()
|
|
|
|
return True
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""
|
|
Make sure we give correct permissions to read/write the thread.
|
|
|
|
Developers of the add-on used in the thread, users in the CC list,
|
|
and users who post to the thread are allowed to access the object.
|
|
|
|
Moreover, other object permissions are also checked agaisnt the ACLs
|
|
of the user.
|
|
"""
|
|
if not request.user.is_authenticated() or obj.read_permission_public:
|
|
return obj.read_permission_public
|
|
|
|
profile = request.amo_user
|
|
user_post = CommunicationNote.objects.filter(author=profile,
|
|
thread=obj)
|
|
user_cc = CommunicationThreadCC.objects.filter(user=profile,
|
|
thread=obj)
|
|
|
|
if user_post.exists() or user_cc.exists():
|
|
return True
|
|
|
|
check_acls = partial(self.check_acls, request, obj)
|
|
|
|
# User is a developer of the add-on and has the permission to read.
|
|
user_is_author = profile.addons.filter(pk=obj.addon_id)
|
|
if obj.read_permission_developer and user_is_author.exists():
|
|
return True
|
|
|
|
if obj.read_permission_reviewer and check_acls('reviewer'):
|
|
return True
|
|
|
|
if (obj.read_permission_senior_reviewer and check_acls(
|
|
'senior_reviewer')):
|
|
return True
|
|
|
|
if (obj.read_permission_mozilla_contact and check_acls(
|
|
'moz_contact')):
|
|
return True
|
|
|
|
if obj.read_permission_staff and check_acls('admin'):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class NotePermission(ThreadPermission):
|
|
|
|
def has_permission(self, request, view):
|
|
if view.action == 'create':
|
|
if not request.user.is_authenticated():
|
|
return False
|
|
|
|
serializer = view.get_serializer(data=request.DATA)
|
|
if not serializer.is_valid():
|
|
return False
|
|
|
|
obj = serializer.object
|
|
# Check if author and user who created this request mismatch.
|
|
if obj.author.id != request.amo_user.id:
|
|
return False
|
|
|
|
# Determine permission to add the note based on the thread
|
|
# permission.
|
|
return ThreadPermission.has_object_permission(self,
|
|
request, view, obj.thread)
|
|
|
|
return True
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
return ThreadPermission.has_object_permission(self, request, view,
|
|
obj.thread)
|
|
|
|
|
|
class ThreadViewSet(ListModelMixin, RetrieveModelMixin, DestroyModelMixin,
|
|
CreateModelMixin, CORSViewSet):
|
|
model = CommunicationThread
|
|
serializer_class = ThreadSerializer
|
|
authentication_classes = (RestOAuthAuthentication,
|
|
RestSharedSecretAuthentication)
|
|
permission_classes = (ThreadPermission,)
|
|
cors_allowed_methods = ['get', 'post']
|
|
|
|
def list(self, request):
|
|
profile = request.amo_user
|
|
# We list all the threads the user has posted a note to.
|
|
notes = profile.comm_notes.values_list('thread', flat=True)
|
|
# We list all the threads where the user has been CC'd.
|
|
cc = profile.comm_thread_cc.values_list('thread', flat=True)
|
|
|
|
# This gives 404 when an app with given slug/id is not found.
|
|
if 'app' in request.GET:
|
|
form = PrepareForm(request.GET)
|
|
if not form.is_valid():
|
|
raise Http404()
|
|
|
|
notes, cc = list(notes), list(cc)
|
|
queryset = CommunicationThread.objects.filter(pk__in=notes + cc,
|
|
addon=form.cleaned_data['app'])
|
|
else:
|
|
# We list all the threads which uses an add-on authored by the
|
|
# user and with read permissions for add-on devs.
|
|
notes, cc = list(notes), list(cc)
|
|
addons = list(profile.addons.values_list('pk', flat=True))
|
|
q_dev = Q(addon__in=addons, read_permission_developer=True)
|
|
queryset = CommunicationThread.objects.filter(
|
|
Q(pk__in=notes + cc) | q_dev)
|
|
|
|
self.queryset = queryset
|
|
return ListModelMixin.list(self, request)
|
|
|
|
|
|
class NoteViewSet(CreateModelMixin, RetrieveModelMixin, DestroyModelMixin,
|
|
CORSViewSet):
|
|
model = CommunicationNote
|
|
serializer_class = NoteSerializer
|
|
authentication_classes = (RestOAuthAuthentication,
|
|
RestSharedSecretAuthentication,)
|
|
permission_classes = (NotePermission,)
|
|
cors_allowed_methods = ['get', 'post', 'delete']
|
|
|
|
def create(self, request):
|
|
res = CreateModelMixin.create(self, request)
|
|
if res.status_code == 201:
|
|
tok = CommunicationThreadToken.objects.create(
|
|
thread=self.object.thread, user=self.object.author)
|
|
log.info('Reply token with UUID %s created.' % (tok.uuid))
|
|
|
|
return res
|