From d97edb464c9235c70134782002c0b9bb266f0c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 5 Jun 2014 16:01:10 +0200 Subject: [PATCH] [HUGE CHANGE] Changed the permissions system --- taiga/auth/api.py | 13 +- taiga/auth/permissions.py | 22 + taiga/base/api.py | 121 -- taiga/base/api/__init__.py | 26 + taiga/base/api/generics.py | 493 ++++++ taiga/base/api/mixins.py | 217 +++ taiga/base/{ => api}/pagination.py | 0 taiga/base/api/permissions.py | 218 +++ taiga/base/api/views.py | 432 +++++ taiga/base/api/viewsets.py | 161 ++ taiga/base/decorators.py | 4 + taiga/base/filters.py | 122 +- taiga/base/serializers.py | 13 + taiga/base/utils/sequence.py | 25 + taiga/permissions/__init__.py | 0 taiga/permissions/models.py | 0 taiga/permissions/permissions.py | 80 + taiga/permissions/service.py | 75 + taiga/projects/api.py | 251 ++- taiga/projects/attachments/api.py | 54 +- taiga/projects/attachments/permissions.py | 66 +- taiga/projects/attachments/views.py | 32 - taiga/projects/history/api.py | 14 +- taiga/projects/history/permissions.py | 24 +- taiga/projects/issues/api.py | 71 +- taiga/projects/issues/permissions.py | 40 +- taiga/projects/milestones/api.py | 14 +- taiga/projects/milestones/permissions.py | 21 +- taiga/projects/milestones/serializers.py | 4 +- taiga/projects/models.py | 39 +- taiga/projects/permissions.py | 180 +- taiga/projects/references/api.py | 18 +- taiga/projects/references/permissions.py | 22 + taiga/projects/serializers.py | 17 +- taiga/projects/services/bulk_update_order.py | 7 + taiga/projects/services/stats.py | 11 +- taiga/projects/tasks/api.py | 11 +- taiga/projects/tasks/permissions.py | 20 +- taiga/projects/userstories/api.py | 23 +- taiga/projects/userstories/permissions.py | 20 +- taiga/projects/userstories/serializers.py | 1 - taiga/projects/votes/models.py | 12 + taiga/projects/votes/services.py | 4 +- taiga/projects/wiki/api.py | 18 +- taiga/projects/wiki/permissions.py | 29 +- taiga/routers.py | 8 - taiga/searches/api.py | 21 +- taiga/timeline/api.py | 12 +- taiga/timeline/permissions.py | 26 + taiga/urls.py | 2 +- taiga/users/admin.py | 2 +- taiga/users/api.py | 77 +- taiga/users/models.py | 9 +- taiga/users/permissions.py | 38 + taiga/users/serializers.py | 9 - taiga/userstorage/api.py | 7 +- taiga/userstorage/permissions.py | 8 +- tests/factories.py | 51 +- .../resources_permissions/__init__.py | 0 .../test_attachment_resources.py | 594 +++++++ .../test_auth_resources.py | 52 + .../test_history_resources.py | 167 ++ .../test_issues_resources.py | 359 ++++ .../test_milestones_resources.py | 263 +++ .../test_projects_choices_resources.py | 1556 +++++++++++++++++ .../test_projects_resource.py | 381 ++++ .../test_resolver_resources.py | 108 ++ .../test_search_resources.py | 109 ++ .../test_storage_resources.py | 125 ++ .../test_tasks_resources.py | 291 +++ .../test_timelines_resources.py | 104 ++ .../test_users_resources.py | 230 +++ .../test_userstories_resources.py | 296 ++++ .../test_wiki_resources.py | 422 +++++ tests/integration/test_attachments.py | 8 +- tests/integration/test_permissions.py | 116 ++ tests/integration/test_project_us.py | 6 +- tests/integration/test_searches.py | 46 +- tests/integration/test_stars.py | 30 +- tests/integration/test_userstorage_api.py | 23 +- tests/unit/test_base_api_permissions.py | 20 + tests/unit/test_permissions.py | 11 + tests/utils.py | 34 + 83 files changed, 7950 insertions(+), 716 deletions(-) create mode 100644 taiga/auth/permissions.py delete mode 100644 taiga/base/api.py create mode 100644 taiga/base/api/__init__.py create mode 100644 taiga/base/api/generics.py create mode 100644 taiga/base/api/mixins.py rename taiga/base/{ => api}/pagination.py (100%) create mode 100644 taiga/base/api/permissions.py create mode 100644 taiga/base/api/views.py create mode 100644 taiga/base/api/viewsets.py create mode 100644 taiga/base/utils/sequence.py create mode 100644 taiga/permissions/__init__.py create mode 100644 taiga/permissions/models.py create mode 100644 taiga/permissions/permissions.py create mode 100644 taiga/permissions/service.py delete mode 100644 taiga/projects/attachments/views.py create mode 100644 taiga/projects/references/permissions.py create mode 100644 taiga/timeline/permissions.py create mode 100644 taiga/users/permissions.py create mode 100644 tests/integration/resources_permissions/__init__.py create mode 100644 tests/integration/resources_permissions/test_attachment_resources.py create mode 100644 tests/integration/resources_permissions/test_auth_resources.py create mode 100644 tests/integration/resources_permissions/test_history_resources.py create mode 100644 tests/integration/resources_permissions/test_issues_resources.py create mode 100644 tests/integration/resources_permissions/test_milestones_resources.py create mode 100644 tests/integration/resources_permissions/test_projects_choices_resources.py create mode 100644 tests/integration/resources_permissions/test_projects_resource.py create mode 100644 tests/integration/resources_permissions/test_resolver_resources.py create mode 100644 tests/integration/resources_permissions/test_search_resources.py create mode 100644 tests/integration/resources_permissions/test_storage_resources.py create mode 100644 tests/integration/resources_permissions/test_tasks_resources.py create mode 100644 tests/integration/resources_permissions/test_timelines_resources.py create mode 100644 tests/integration/resources_permissions/test_users_resources.py create mode 100644 tests/integration/resources_permissions/test_userstories_resources.py create mode 100644 tests/integration/resources_permissions/test_wiki_resources.py create mode 100644 tests/integration/test_permissions.py create mode 100644 tests/unit/test_base_api_permissions.py create mode 100644 tests/unit/test_permissions.py diff --git a/taiga/auth/api.py b/taiga/auth/api.py index 11a0e41d..b0d0365d 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -21,11 +21,10 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from rest_framework.response import Response -from rest_framework.permissions import AllowAny from rest_framework import status -from rest_framework import viewsets from rest_framework import serializers +from taiga.base.api import viewsets from taiga.base.decorators import list_route from taiga.base import exceptions as exc from taiga.base.connectors import github @@ -41,6 +40,8 @@ from .services import public_register from .services import github_register from .services import make_auth_response_data +from .permissions import AuthPermission + def _parse_data(data:dict, *, cls): """ @@ -95,7 +96,7 @@ def parse_register_type(userdata:dict) -> str: class AuthViewSet(viewsets.ViewSet): - permission_classes = (AllowAny,) + permission_classes = (AuthPermission,) def _public_register(self, request): if not settings.PUBLIC_REGISTER_ENABLED: @@ -123,8 +124,10 @@ class AuthViewSet(viewsets.ViewSet): data = make_auth_response_data(user) return Response(data, status=status.HTTP_201_CREATED) - @list_route(methods=["POST"], permission_classes=[AllowAny]) + @list_route(methods=["POST"]) def register(self, request, **kwargs): + self.check_permissions(request, 'register', None) + type = request.DATA.get("type", None) if type == "public": return self._public_register(request) @@ -157,6 +160,8 @@ class AuthViewSet(viewsets.ViewSet): # Login view: /api/v1/auth def create(self, request, **kwargs): + self.check_permissions(request, 'create', None) + type = request.DATA.get("type", None) if type == "normal": return self._login(request) diff --git a/taiga/auth/permissions.py b/taiga/auth/permissions.py new file mode 100644 index 00000000..a22ef747 --- /dev/null +++ b/taiga/auth/permissions.py @@ -0,0 +1,22 @@ +# Copyright (C) 2014 Andrey Antukh # Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from taiga.base.api.permissions import ResourcePermission, AllowAny + + +class AuthPermission(ResourcePermission): + create_perms = AllowAny() + register_perms = AllowAny() diff --git a/taiga/base/api.py b/taiga/base/api.py deleted file mode 100644 index b3b0d694..00000000 --- a/taiga/base/api.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from django.db import transaction - -from rest_framework import viewsets -from rest_framework import status -from rest_framework import mixins -from rest_framework.response import Response - -from . import pagination -from . import serializers - - -# Transactional version of rest framework mixins. - -class CreateModelMixin(mixins.CreateModelMixin): - @transaction.atomic - def create(self, *args, **kwargs): - return super().create(*args, **kwargs) - - -class RetrieveModelMixin(mixins.RetrieveModelMixin): - @transaction.atomic - def retrieve(self, *args, **kwargs): - return super().retrieve(*args, **kwargs) - - -class UpdateModelMixin(mixins.UpdateModelMixin): - @transaction.atomic - def update(self, *args, **kwargs): - return super().update(*args, **kwargs) - - @transaction.atomic - def partial_update(self, request, *args, **kwargs): - return super().partial_update(request, *args, **kwargs) - -class ListModelMixin(mixins.ListModelMixin): - @transaction.atomic - def list(self, *args, **kwargs): - return super().list(*args, **kwargs) - - -class DestroyModelMixin(mixins.DestroyModelMixin): - @transaction.atomic - def destroy(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - -# Other mixins (what they are doing here?) - - -class PreconditionMixin(object): - def pre_conditions_on_save(self, obj): - pass - - def pre_conditions_on_delete(self, obj): - pass - - def pre_save(self, obj): - super().pre_save(obj) - self.pre_conditions_on_save(obj) - - def pre_delete(self, obj): - super().pre_delete(obj) - self.pre_conditions_on_delete(obj) - - -class DetailAndListSerializersMixin(object): - """ - Use a diferent serializer class to the list action. - """ - list_serializer_class = None - - def get_serializer_class(self): - if self.action == "list" and self.list_serializer_class: - return self.list_serializer_class - return super().get_serializer_class() - - -# Own subclasses of django rest framework viewsets - -class ModelCrudViewSet(DetailAndListSerializersMixin, - PreconditionMixin, - pagination.HeadersPaginationMixin, - pagination.ConditionalPaginationMixin, - CreateModelMixin, - RetrieveModelMixin, - UpdateModelMixin, - DestroyModelMixin, - ListModelMixin, - viewsets.GenericViewSet): - pass - - -class ModelListViewSet(DetailAndListSerializersMixin, - PreconditionMixin, - pagination.HeadersPaginationMixin, - pagination.ConditionalPaginationMixin, - RetrieveModelMixin, - ListModelMixin, - viewsets.GenericViewSet): - pass - - -class GenericViewSet(viewsets.GenericViewSet): - pass diff --git a/taiga/base/api/__init__.py b/taiga/base/api/__init__.py new file mode 100644 index 00000000..e9bd5aca --- /dev/null +++ b/taiga/base/api/__init__.py @@ -0,0 +1,26 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# This code is partially taken from django-rest-framework: +# Copyright (c) 2011-2014, Tom Christie + +from .viewsets import ModelListViewSet +from .viewsets import ModelCrudViewSet +from .viewsets import GenericViewSet + +__all__ = ["ModelCrudViewSet", + "ModelListViewSet", + "GenericViewSet"] diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py new file mode 100644 index 00000000..6cee8f18 --- /dev/null +++ b/taiga/base/api/generics.py @@ -0,0 +1,493 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# This code is partially taken from django-rest-framework: +# Copyright (c) 2011-2014, Tom Christie + +import warnings + +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.core.paginator import Paginator, InvalidPage +from django.http import Http404 +from django.shortcuts import get_object_or_404 as _get_object_or_404 +from django.utils.translation import ugettext as _ + +from rest_framework import exceptions +from rest_framework.request import clone_request +from rest_framework.settings import api_settings + +from . import views +from . import mixins + + +def strict_positive_int(integer_string, cutoff=None): + """ + Cast a string to a strictly positive integer. + """ + ret = int(integer_string) + if ret <= 0: + raise ValueError() + if cutoff: + ret = min(ret, cutoff) + return ret + + +def get_object_or_404(queryset, *filter_args, **filter_kwargs): + """ + Same as Django's standard shortcut, but make sure to raise 404 + if the filter_kwargs don't match the required types. + """ + try: + return _get_object_or_404(queryset, *filter_args, **filter_kwargs) + except (TypeError, ValueError): + raise Http404 + + +class GenericAPIView(views.APIView): + """ + Base class for all other generic views. + """ + + # You'll need to either set these attributes, + # or override `get_queryset()`/`get_serializer_class()`. + queryset = None + serializer_class = None + + # This shortcut may be used instead of setting either or both + # of the `queryset`/`serializer_class` attributes, although using + # the explicit style is generally preferred. + model = None + + # If you want to use object lookups other than pk, set this attribute. + # For more complex lookup requirements override `get_object()`. + lookup_field = 'pk' + lookup_url_kwarg = None + + # Pagination settings + paginate_by = api_settings.PAGINATE_BY + paginate_by_param = api_settings.PAGINATE_BY_PARAM + max_paginate_by = api_settings.MAX_PAGINATE_BY + pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS + page_kwarg = 'page' + + # The filter backend classes to use for queryset filtering + filter_backends = api_settings.DEFAULT_FILTER_BACKENDS + + # The following attributes may be subject to change, + # and should be considered private API. + model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + paginator_class = Paginator + + ###################################### + # These are pending deprecation... + + pk_url_kwarg = 'pk' + slug_url_kwarg = 'slug' + slug_field = 'slug' + allow_empty = True + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def get_serializer(self, instance=None, data=None, + files=None, many=False, partial=False): + """ + Return the serializer instance that should be used for validating and + deserializing input, and for serializing output. + """ + serializer_class = self.get_serializer_class() + context = self.get_serializer_context() + return serializer_class(instance, data=data, files=files, + many=many, partial=partial, context=context) + + def get_pagination_serializer(self, page): + """ + Return a serializer instance to use with paginated data. + """ + class SerializerClass(self.pagination_serializer_class): + class Meta: + object_serializer_class = self.get_serializer_class() + + pagination_serializer_class = SerializerClass + context = self.get_serializer_context() + return pagination_serializer_class(instance=page, context=context) + + def paginate_queryset(self, queryset, page_size=None): + """ + Paginate a queryset if required, either returning a page object, + or `None` if pagination is not configured for this view. + """ + deprecated_style = False + if page_size is not None: + warnings.warn('The `page_size` parameter to `paginate_queryset()` ' + 'is due to be deprecated. ' + 'Note that the return style of this method is also ' + 'changed, and will simply return a page object ' + 'when called without a `page_size` argument.', + PendingDeprecationWarning, stacklevel=2) + deprecated_style = True + else: + # Determine the required page size. + # If pagination is not configured, simply return None. + page_size = self.get_paginate_by() + if not page_size: + return None + + if not self.allow_empty: + warnings.warn( + 'The `allow_empty` parameter is due to be deprecated. ' + 'To use `allow_empty=False` style behavior, You should override ' + '`get_queryset()` and explicitly raise a 404 on empty querysets.', + PendingDeprecationWarning, stacklevel=2 + ) + + paginator = self.paginator_class(queryset, page_size, + allow_empty_first_page=self.allow_empty) + page_kwarg = self.kwargs.get(self.page_kwarg) + page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) + page = page_kwarg or page_query_param or 1 + try: + page_number = paginator.validate_number(page) + except InvalidPage: + if page == 'last': + page_number = paginator.num_pages + else: + raise Http404(_("Page is not 'last', nor can it be converted to an int.")) + try: + page = paginator.page(page_number) + except InvalidPage as e: + raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { + 'page_number': page_number, + 'message': str(e) + }) + + if deprecated_style: + return (paginator, page, page.object_list, page.has_other_pages()) + return page + + def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + + You are unlikely to want to override this method, although you may need + to call it either from a list view, or from a custom `get_object` + method if you want to apply the configured filtering backend to the + default queryset. + """ + for backend in self.get_filter_backends(): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def get_filter_backends(self): + """ + Returns the list of filter backends that this view requires. + """ + filter_backends = self.filter_backends or [] + if not filter_backends and hasattr(self, 'filter_backend'): + raise RuntimeException('The `filter_backend` attribute and `FILTER_BACKEND` setting ' + 'are due to be deprecated in favor of a `filter_backends` ' + 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' + 'a *list* of filter backend classes.') + return filter_backends + + + ######################## + ### The following methods provide default implementations + ### that you may want to override for more complex cases. + + def get_paginate_by(self, queryset=None): + """ + Return the size of pages to use with pagination. + + If `PAGINATE_BY_PARAM` is set it will attempt to get the page size + from a named query parameter in the url, eg. ?page_size=100 + + Otherwise defaults to using `self.paginate_by`. + """ + if queryset is not None: + raise RuntimeException('The `queryset` parameter to `get_paginate_by()` ' + 'is due to be deprecated.') + if self.paginate_by_param: + try: + return strict_positive_int( + self.request.QUERY_PARAMS[self.paginate_by_param], + cutoff=self.max_paginate_by + ) + except (KeyError, ValueError): + pass + + return self.paginate_by + + def get_serializer_class(self): + if self.action == "list" and hasattr(self, "list_serializer_class"): + return self.list_serializer_class + + serializer_class = self.serializer_class + if serializer_class is not None: + return serializer_class + + assert self.model is not None, \ + "'%s' should either include a 'serializer_class' attribute, " \ + "or use the 'model' attribute as a shortcut for " \ + "automatically generating a serializer class." \ + % self.__class__.__name__ + + class DefaultSerializer(self.model_serializer_class): + class Meta: + model = self.model + return DefaultSerializer + + def get_queryset(self): + """ + Get the list of items for this view. + This must be an iterable, and may be a queryset. + Defaults to using `self.queryset`. + + You may want to override this if you need to provide different + querysets depending on the incoming request. + + (Eg. return a list of items that is specific to the user) + """ + if self.queryset is not None: + return self.queryset._clone() + + if self.model is not None: + return self.model._default_manager.all() + + raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" + % self.__class__.__name__) + + def get_object(self, queryset=None): + """ + Returns the object the view is displaying. + + You may want to override this if you need to provide non-standard + queryset lookups. Eg if objects are referenced using multiple + keyword arguments in the url conf. + """ + # Determine the base queryset to use. + if queryset is None: + queryset = self.filter_queryset(self.get_queryset()) + else: + # NOTE: explicit exception for avoid and fix + # usage of deprecated way of get_object + raise RuntimeException("DEPRECATED") + + # Perform the lookup filtering. + # Note that `pk` and `slug` are deprecated styles of lookup filtering. + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) + pk = self.kwargs.get(self.pk_url_kwarg, None) + slug = self.kwargs.get(self.slug_url_kwarg, None) + + if lookup is not None: + filter_kwargs = {self.lookup_field: lookup} + elif pk is not None and self.lookup_field == 'pk': + raise RuntimeException('The `pk_url_kwarg` attribute is due to be deprecated. ' + 'Use the `lookup_field` attribute instead') + elif slug is not None and self.lookup_field == 'pk': + raise RuntimeException('The `slug_url_kwarg` attribute is due to be deprecated. ' + 'Use the `lookup_field` attribute instead') + else: + raise ImproperlyConfigured( + 'Expected view %s to be called with a URL keyword argument ' + 'named "%s". Fix your URL conf, or set the `.lookup_field` ' + 'attribute on the view correctly.' % + (self.__class__.__name__, self.lookup_field) + ) + + obj = get_object_or_404(queryset, **filter_kwargs) + return obj + + def get_object_or_none(self): + try: + return self.get_object() + except Http404: + return None + + ######################## + ### The following are placeholder methods, + ### and are intended to be overridden. + ### + ### The are not called by GenericAPIView directly, + ### but are used by the mixin methods. + + def pre_conditions_on_save(self, obj): + """ + Placeholder method called by mixins before save for check + some conditions before save. + """ + pass + + def pre_conditions_on_delete(self, obj): + """ + Placeholder method called by mixins before delete for check + some conditions before delete. + """ + pass + + def pre_save(self, obj): + """ + Placeholder method for calling before saving an object. + + May be used to set attributes on the object that are implicit + in either the request, or the url. + """ + pass + + def post_save(self, obj, created=False): + """ + Placeholder method for calling after saving an object. + """ + pass + + def pre_delete(self, obj): + """ + Placeholder method for calling before deleting an object. + """ + pass + + def post_delete(self, obj): + """ + Placeholder method for calling after deleting an object. + """ + pass + + +########################################################## +### Concrete view classes that provide method handlers ### +### by composing the mixin classes with the base view. ### +### NOTE: not used by taiga. ### +########################################################## + +class CreateAPIView(mixins.CreateModelMixin, + GenericAPIView): + + """ + Concrete view for creating a model instance. + """ + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + +class ListAPIView(mixins.ListModelMixin, + GenericAPIView): + """ + Concrete view for listing a queryset. + """ + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + +class RetrieveAPIView(mixins.RetrieveModelMixin, + GenericAPIView): + """ + Concrete view for retrieving a model instance. + """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + +class DestroyAPIView(mixins.DestroyModelMixin, + GenericAPIView): + + """ + Concrete view for deleting a model instance. + """ + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class UpdateAPIView(mixins.UpdateModelMixin, + GenericAPIView): + + """ + Concrete view for updating a model instance. + """ + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + +class ListCreateAPIView(mixins.ListModelMixin, + mixins.CreateModelMixin, + GenericAPIView): + """ + Concrete view for listing a queryset or creating a model instance. + """ + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + +class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + GenericAPIView): + """ + Concrete view for retrieving, updating a model instance. + """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + +class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + GenericAPIView): + """ + Concrete view for retrieving or deleting a model instance. + """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericAPIView): + """ + Concrete view for retrieving, updating or deleting a model instance. + """ + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py new file mode 100644 index 00000000..ec31fa94 --- /dev/null +++ b/taiga/base/api/mixins.py @@ -0,0 +1,217 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# This code is partially taken from django-rest-framework: +# Copyright (c) 2011-2014, Tom Christie + +import warnings + +from django.core.exceptions import ValidationError +from django.http import Http404 +from django.db import transaction as tx + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.request import clone_request +from rest_framework.settings import api_settings + + +def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): + """ + Given a model instance, and an optional pk and slug field, + return the full list of all other field names on that model. + + For use when performing full_clean on a model instance, + so we only clean the required fields. + """ + include = [] + + if pk: + # Pending deprecation + pk_field = obj._meta.pk + while pk_field.rel: + pk_field = pk_field.rel.to._meta.pk + include.append(pk_field.name) + + if slug_field: + # Pending deprecation + include.append(slug_field) + + if lookup_field and lookup_field != 'pk': + include.append(lookup_field) + + return [field.name for field in obj._meta.fields if field.name not in include] + + +class CreateModelMixin(object): + """ + Create a model instance. + """ + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.DATA, files=request.FILES) + + if serializer.is_valid(): + self.check_permissions(request, 'create', serializer.object) + + self.pre_save(serializer.object) + self.pre_conditions_on_save(serializer.object) + self.object = serializer.save(force_insert=True) + self.post_save(self.object, created=True) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, + headers=headers) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def get_success_headers(self, data): + try: + return {'Location': data[api_settings.URL_FIELD_NAME]} + except (TypeError, KeyError): + return {} + + +class ListModelMixin(object): + """ + List a queryset. + """ + empty_error = "Empty list and '%(class_name)s.allow_empty' is False." + + def list(self, request, *args, **kwargs): + self.object_list = self.filter_queryset(self.get_queryset()) + + # Default is to allow empty querysets. This can be altered by setting + # `.allow_empty = False`, to raise 404 errors on empty querysets. + if not self.allow_empty and not self.object_list: + warnings.warn( + 'The `allow_empty` parameter is due to be deprecated. ' + 'To use `allow_empty=False` style behavior, You should override ' + '`get_queryset()` and explicitly raise a 404 on empty querysets.', + PendingDeprecationWarning + ) + class_name = self.__class__.__name__ + error_msg = self.empty_error % {'class_name': class_name} + raise Http404(error_msg) + + # Switch between paginated or standard style responses + page = self.paginate_queryset(self.object_list) + if page is not None: + serializer = self.get_pagination_serializer(page) + else: + serializer = self.get_serializer(self.object_list, many=True) + + return Response(serializer.data) + + +class RetrieveModelMixin(object): + """ + Retrieve a model instance. + """ + def retrieve(self, request, *args, **kwargs): + self.object = self.get_object_or_none() + + self.check_permissions(request, 'retrieve', self.object) + + if self.object is None: + raise Http404 + + serializer = self.get_serializer(self.object) + return Response(serializer.data) + + +class UpdateModelMixin(object): + """ + Update a model instance. + """ + + @tx.atomic + def update(self, request, *args, **kwargs): + partial = kwargs.pop('partial', False) + self.object = self.get_object_or_none() + + self.check_permissions(request, 'update', self.object) + + serializer = self.get_serializer(self.object, data=request.DATA, + files=request.FILES, partial=partial) + + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # Hooks + try: + self.pre_save(serializer.object) + self.pre_conditions_on_save(serializer.object) + except ValidationError as err: + # full_clean on model instance may be called in pre_save, + # so we have to handle eventual errors. + return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) + + if self.object is None: + self.object = serializer.save(force_insert=True) + self.post_save(self.object, created=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + self.object = serializer.save(force_update=True) + self.post_save(self.object, created=False) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, *args, **kwargs): + kwargs['partial'] = True + return self.update(request, *args, **kwargs) + + def pre_save(self, obj): + """ + Set any attributes on the object that are implicit in the request. + """ + # pk and/or slug attributes are implicit in the URL. + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) + pk = self.kwargs.get(self.pk_url_kwarg, None) + slug = self.kwargs.get(self.slug_url_kwarg, None) + slug_field = slug and self.slug_field or None + + if lookup: + setattr(obj, self.lookup_field, lookup) + + if pk: + setattr(obj, 'pk', pk) + + if slug: + setattr(obj, slug_field, slug) + + # Ensure we clean the attributes so that we don't eg return integer + # pk using a string representation, as provided by the url conf kwarg. + if hasattr(obj, 'full_clean'): + exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field) + obj.full_clean(exclude) + + +class DestroyModelMixin(object): + """ + Destroy a model instance. + """ + @tx.atomic + def destroy(self, request, *args, **kwargs): + obj = self.get_object_or_none() + self.check_permissions(request, 'destroy', obj) + + if obj is None: + raise Http404 + + self.pre_delete(obj) + self.pre_conditions_on_delete(obj) + obj.delete() + self.post_delete(obj) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/taiga/base/pagination.py b/taiga/base/api/pagination.py similarity index 100% rename from taiga/base/pagination.py rename to taiga/base/api/pagination.py diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py new file mode 100644 index 00000000..d0a5365f --- /dev/null +++ b/taiga/base/api/permissions.py @@ -0,0 +1,218 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import abc + +from taiga.base.utils import sequence as sq +from taiga.permissions.service import user_has_perm, is_project_owner +from django.db.models.loading import get_model + + +###################################################################### +# Base permissiones definition +###################################################################### + +class ResourcePermission(object): + """ + Base class for define resource permissions. + """ + + enought_perms = None + global_perms = None + retrieve_perms = None + create_perms = None + update_perms = None + destroy_perms = None + list_perms = None + + def __init__(self, request, view): + self.request = request + self.view = view + + def check_permissions(self, action:str, obj:object=None): + permset = getattr(self, "{}_perms".format(action)) + + if isinstance(permset, (list, tuple)): + permset = reduce(lambda acc, v: acc & v, permset) + elif permset is None: + # Use empty operator that always return true with + # empty components. + permset = And() + elif isinstance(permset, PermissionComponent): + # Do nothing + pass + elif inspect.isclass(permset) and issubclass(permset, PermissionComponent): + permset = permset() + else: + raise RuntimeError("Invalid permission definition.") + + if self.global_perms: + permset = (self.global_perms & permset) + + if self.enought_perms: + permset = (self.enought_perms | permset) + + return permset.check_permissions(request=self.request, + view=self.view, + obj=obj) + + +class PermissionComponent(object, metaclass=abc.ABCMeta): + @abc.abstractmethod + def check_permissions(self, request, view, obj=None): + pass + + def __invert__(self): + return Not(self) + + def __and__(self, component): + return And(self, component) + + def __or__(self, component): + return Or(self, component) + + +class PermissionOperator(PermissionComponent): + """ + Base class for all logical operators for compose + components. + """ + + def __init__(self, *components): + self.components = tuple(components) + + +class Not(PermissionOperator): + """ + Negation operator as permission composable component. + """ + + # Overwrites the default constructor for fix + # to one parameter instead of variable list of them. + def __init__(self, component): + super().__init__(component) + + def check_permissions(self, *args, **kwargs): + component = sq.first(self.components) + return (not component.check_permissions(*args, **kwargs)) + + +class Or(PermissionOperator): + """ + Or logical operator as permission component. + """ + + def check_permissions(self, *args, **kwargs): + valid = False + + for component in self.components: + if component.check_permissions(*args, **kwargs): + valid = True + break + + return valid + + +class And(PermissionOperator): + """ + And logical operator as permission component. + """ + + def check_permissions(self, *args, **kwargs): + valid = True + + for component in self.components: + if not component.check_permissions(*args, **kwargs): + valid = False + break + + return valid + + +###################################################################### +# Generic components. +###################################################################### + +class AllowAny(PermissionComponent): + def check_permissions(self, request, view, obj=None): + return True + + +class DenyAll(PermissionComponent): + def check_permissions(self, request, view, obj=None): + return False + + +class IsAuthenticated(PermissionComponent): + def check_permissions(self, request, view, obj=None): + return request.user and request.user.is_authenticated() + + +class IsSuperUser(PermissionComponent): + def check_permissions(self, request, view, obj=None): + return request.user and request.user.is_authenticated() and request.user.is_superuser + + +class HasProjectPerm(PermissionComponent): + def __init__(self, perm, *components): + self.project_perm = perm + super().__init__(*components) + + def check_permissions(self, request, view, obj=None): + return user_has_perm(request.user, self.project_perm, obj) + + +class HasProjectParamAndPerm(PermissionComponent): + def __init__(self, perm, *components): + self.project_perm = perm + super().__init__(*components) + + def check_permissions(self, request, view, obj=None): + Project = get_model('projects', 'Project') + project_id = request.QUERY_PARAMS.get("project", None) + try: + project = Project.objects.get(pk=project_id) + except Project.DoesNotExist: + return False + return user_has_perm(request.user, self.project_perm, project) + + +class HasMandatoryParam(PermissionComponent): + def __init__(self, param, *components): + self.mandatory_param = param + super().__init__(*components) + + def check_permissions(self, request, view, obj=None): + param = request.GET.get(self.mandatory_param, None) + if param: + return True + return False + + +class IsProjectOwner(PermissionComponent): + def check_permissions(self, request, view, obj=None): + return is_project_owner(request.user, obj) + + +###################################################################### +# Generic permissions. +###################################################################### + +class AllowAnyPermission(ResourcePermission): + enought_perms = AllowAny() + +class IsAuthenticatedPermission(ResourcePermission): + enought_perms = IsAuthenticated() diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py new file mode 100644 index 00000000..19a81b79 --- /dev/null +++ b/taiga/base/api/views.py @@ -0,0 +1,432 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# This code is partially taken from django-rest-framework: +# Copyright (c) 2011-2014, Tom Christie + +from django.core.exceptions import PermissionDenied +from django.http import Http404 +from django.utils.datastructures import SortedDict +from django.views.decorators.csrf import csrf_exempt + +from rest_framework import status, exceptions +from rest_framework.compat import smart_text, HttpResponseBase, View +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.settings import api_settings +from rest_framework.utils import formatting + +from taiga.base.utils.iterators import as_tuple + + +def get_view_name(view_cls, suffix=None): + """ + Given a view class, return a textual name to represent the view. + This name is used in the browsable API, and in OPTIONS responses. + + This function is the default for the `VIEW_NAME_FUNCTION` setting. + """ + name = view_cls.__name__ + name = formatting.remove_trailing_string(name, 'View') + name = formatting.remove_trailing_string(name, 'ViewSet') + name = formatting.camelcase_to_spaces(name) + if suffix: + name += ' ' + suffix + + return name + +def get_view_description(view_cls, html=False): + """ + Given a view class, return a textual description to represent the view. + This name is used in the browsable API, and in OPTIONS responses. + + This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting. + """ + description = view_cls.__doc__ or '' + description = formatting.dedent(smart_text(description)) + if html: + return formatting.markup_description(description) + return description + + +def exception_handler(exc): + """ + Returns the response that should be used for any given exception. + + By default we handle the REST framework `APIException`, and also + Django's builtin `Http404` and `PermissionDenied` exceptions. + + Any unhandled exceptions may return `None`, which will cause a 500 error + to be raised. + """ + if isinstance(exc, exceptions.APIException): + headers = {} + if getattr(exc, 'auth_header', None): + headers['WWW-Authenticate'] = exc.auth_header + if getattr(exc, 'wait', None): + headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait + + return Response({'detail': exc.detail}, + status=exc.status_code, + headers=headers) + + elif isinstance(exc, Http404): + return Response({'detail': 'Not found'}, + status=status.HTTP_404_NOT_FOUND) + + elif isinstance(exc, PermissionDenied): + return Response({'detail': 'Permission denied'}, + status=status.HTTP_403_FORBIDDEN) + + # Note: Unhandled exceptions will raise a 500 error. + return None + + +class APIView(View): + # The following policies may be set at either globally, or per-view. + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES + permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS + + # Allow dependancy injection of other settings to make testing easier. + settings = api_settings + + @classmethod + def as_view(cls, **initkwargs): + """ + Store the original class on the view function. + + This allows us to discover information about the view when we do URL + reverse lookups. Used for breadcrumb generation. + """ + view = super(APIView, cls).as_view(**initkwargs) + view.cls = cls + return view + + @property + def allowed_methods(self): + """ + Wrap Django's private `_allowed_methods` interface in a public property. + """ + return self._allowed_methods() + + @property + def default_response_headers(self): + headers = { + 'Allow': ', '.join(self.allowed_methods), + } + if len(self.renderer_classes) > 1: + headers['Vary'] = 'Accept' + return headers + + + def http_method_not_allowed(self, request, *args, **kwargs): + """ + If `request.method` does not correspond to a handler method, + determine what kind of exception to raise. + """ + raise exceptions.MethodNotAllowed(request.method) + + def permission_denied(self, request): + """ + If request is not permitted, determine what kind of exception to raise. + """ + if not request.successful_authenticator: + raise exceptions.NotAuthenticated() + raise exceptions.PermissionDenied() + + def throttled(self, request, wait): + """ + If request is throttled, determine what kind of exception to raise. + """ + raise exceptions.Throttled(wait) + + def get_authenticate_header(self, request): + """ + If a request is unauthenticated, determine the WWW-Authenticate + header to use for 401 responses, if any. + """ + authenticators = self.get_authenticators() + if authenticators: + return authenticators[0].authenticate_header(request) + + def get_parser_context(self, http_request): + """ + Returns a dict that is passed through to Parser.parse(), + as the `parser_context` keyword argument. + """ + # Note: Additionally `request` and `encoding` will also be added + # to the context by the Request object. + return { + 'view': self, + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}) + } + + def get_renderer_context(self): + """ + Returns a dict that is passed through to Renderer.render(), + as the `renderer_context` keyword argument. + """ + # Note: Additionally 'response' will also be added to the context, + # by the Response object. + return { + 'view': self, + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}), + 'request': getattr(self, 'request', None) + } + + def get_view_name(self): + """ + Return the view name, as used in OPTIONS responses and in the + browsable API. + """ + func = self.settings.VIEW_NAME_FUNCTION + return func(self.__class__, getattr(self, 'suffix', None)) + + def get_view_description(self, html=False): + """ + Return some descriptive text for the view, as used in OPTIONS responses + and in the browsable API. + """ + func = self.settings.VIEW_DESCRIPTION_FUNCTION + return func(self.__class__, html) + + # API policy instantiation methods + + def get_format_suffix(self, **kwargs): + """ + Determine if the request includes a '.json' style format suffix + """ + if self.settings.FORMAT_SUFFIX_KWARG: + return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG) + + def get_renderers(self): + """ + Instantiates and returns the list of renderers that this view can use. + """ + return [renderer() for renderer in self.renderer_classes] + + def get_parsers(self): + """ + Instantiates and returns the list of parsers that this view can use. + """ + return [parser() for parser in self.parser_classes] + + def get_authenticators(self): + """ + Instantiates and returns the list of authenticators that this view can use. + """ + return [auth() for auth in self.authentication_classes] + + @as_tuple + def get_permissions(self): + """ + Instantiates and returns the list of permissions that this view requires. + """ + for permcls in self.permission_classes: + instance = permcls(request=self.request, + view=self) + yield instance + + def get_throttles(self): + """ + Instantiates and returns the list of throttles that this view uses. + """ + return [throttle() for throttle in self.throttle_classes] + + def get_content_negotiator(self): + """ + Instantiate and return the content negotiation class to use. + """ + if not getattr(self, '_negotiator', None): + self._negotiator = self.content_negotiation_class() + return self._negotiator + + # API policy implementation methods + + def perform_content_negotiation(self, request, force=False): + """ + Determine which renderer and media type to use render the response. + """ + renderers = self.get_renderers() + conneg = self.get_content_negotiator() + + try: + return conneg.select_renderer(request, renderers, self.format_kwarg) + except Exception: + if force: + return (renderers[0], renderers[0].media_type) + raise + + def perform_authentication(self, request): + """ + Perform authentication on the incoming request. + + Note that if you override this and simply 'pass', then authentication + will instead be performed lazily, the first time either + `request.user` or `request.auth` is accessed. + """ + request.user + + def check_permissions(self, request, action, obj=None): + for permission in self.get_permissions(): + if not permission.check_permissions(action=action, obj=obj): + self.permission_denied(request) + + def check_throttles(self, request): + """ + Check if request should be throttled. + Raises an appropriate exception if the request is throttled. + """ + for throttle in self.get_throttles(): + if not throttle.allow_request(request, self): + self.throttled(request, throttle.wait()) + + # Dispatch methods + + def initialize_request(self, request, *args, **kwargs): + """ + Returns the initial request object. + """ + parser_context = self.get_parser_context(request) + + return Request(request, + parsers=self.get_parsers(), + authenticators=self.get_authenticators(), + negotiator=self.get_content_negotiator(), + parser_context=parser_context) + + def initial(self, request, *args, **kwargs): + """ + Runs anything that needs to occur prior to calling the method handler. + """ + self.format_kwarg = self.get_format_suffix(**kwargs) + + # Ensure that the incoming request is permitted + self.perform_authentication(request) + self.check_throttles(request) + + # Perform content negotiation and store the accepted info on the request + neg = self.perform_content_negotiation(request) + request.accepted_renderer, request.accepted_media_type = neg + + def finalize_response(self, request, response, *args, **kwargs): + """ + Returns the final response object. + """ + # Make the error obvious if a proper response is not returned + assert isinstance(response, HttpResponseBase), ( + 'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` ' + 'to be returned from the view, but received a `%s`' + % type(response) + ) + + if isinstance(response, Response): + if not getattr(request, 'accepted_renderer', None): + neg = self.perform_content_negotiation(request, force=True) + request.accepted_renderer, request.accepted_media_type = neg + + response.accepted_renderer = request.accepted_renderer + response.accepted_media_type = request.accepted_media_type + response.renderer_context = self.get_renderer_context() + + for key, value in self.headers.items(): + response[key] = value + + return response + + def handle_exception(self, exc): + """ + Handle any exception that occurs, by returning an appropriate response, + or re-raising the error. + """ + if isinstance(exc, (exceptions.NotAuthenticated, + exceptions.AuthenticationFailed)): + # WWW-Authenticate header for 401 responses, else coerce to 403 + auth_header = self.get_authenticate_header(self.request) + + if auth_header: + exc.auth_header = auth_header + else: + exc.status_code = status.HTTP_403_FORBIDDEN + + response = self.settings.EXCEPTION_HANDLER(exc) + + if response is None: + raise + + response.exception = True + return response + + # Note: session based authentication is explicitly CSRF validated, + # all other authentication is CSRF exempt. + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + """ + `.dispatch()` is pretty much the same as Django's regular dispatch, + but with extra hooks for startup, finalize, and exception handling. + """ + self.args = args + self.kwargs = kwargs + request = self.initialize_request(request, *args, **kwargs) + self.request = request + self.headers = self.default_response_headers + + try: + self.initial(request, *args, **kwargs) + + # Get the appropriate handler method + if request.method.lower() in self.http_method_names: + handler = getattr(self, request.method.lower(), + self.http_method_not_allowed) + else: + handler = self.http_method_not_allowed + + response = handler(request, *args, **kwargs) + + except Exception as exc: + response = self.handle_exception(exc) + + self.response = self.finalize_response(request, response, *args, **kwargs) + return self.response + + def options(self, request, *args, **kwargs): + """ + Handler method for HTTP 'OPTIONS' request. + We may as well implement this as Django will otherwise provide + a less useful default implementation. + """ + return Response(self.metadata(request), status=status.HTTP_200_OK) + + def metadata(self, request): + """ + Return a dictionary of metadata about the view. + Used to return responses for OPTIONS requests. + """ + # By default we can't provide any form-like information, however the + # generic views override this implementation and add additional + # information for POST and PUT methods, based on the serializer. + ret = SortedDict() + ret['name'] = self.get_view_name() + ret['description'] = self.get_view_description() + ret['renders'] = [renderer.media_type for renderer in self.renderer_classes] + ret['parses'] = [parser.media_type for parser in self.parser_classes] + return ret diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py new file mode 100644 index 00000000..40619056 --- /dev/null +++ b/taiga/base/api/viewsets.py @@ -0,0 +1,161 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# This code is partially taken from django-rest-framework: +# Copyright (c) 2011-2014, Tom Christie + +from functools import update_wrapper +from django.utils.decorators import classonlymethod + +from . import views +from . import mixins +from . import generics +from . import pagination + + +class ViewSetMixin(object): + """ + This is the magic. + + Overrides `.as_view()` so that it takes an `actions` keyword that performs + the binding of HTTP methods to actions on the Resource. + + For example, to create a concrete view binding the 'GET' and 'POST' methods + to the 'list' and 'create' actions... + + view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) + """ + + @classonlymethod + def as_view(cls, actions=None, **initkwargs): + """ + Because of the way class based views create a closure around the + instantiated view, we need to totally reimplement `.as_view`, + and slightly modify the view function that is created and returned. + """ + # The suffix initkwarg is reserved for identifing the viewset type + # eg. 'List' or 'Instance'. + cls.suffix = None + + # sanitize keyword arguments + for key in initkwargs: + if key in cls.http_method_names: + raise TypeError("You tried to pass in the %s method name as a " + "keyword argument to %s(). Don't do that." + % (key, cls.__name__)) + if not hasattr(cls, key): + raise TypeError("%s() received an invalid keyword %r" % ( + cls.__name__, key)) + + def view(request, *args, **kwargs): + self = cls(**initkwargs) + # We also store the mapping of request methods to actions, + # so that we can later set the action attribute. + # eg. `self.action = 'list'` on an incoming GET request. + self.action_map = actions + + # Bind methods to actions + # This is the bit that's different to a standard view + for method, action in actions.items(): + handler = getattr(self, action) + setattr(self, method, handler) + + # Patch this in as it's otherwise only present from 1.5 onwards + if hasattr(self, 'get') and not hasattr(self, 'head'): + self.head = self.get + + # And continue as usual + return self.dispatch(request, *args, **kwargs) + + # take name and docstring from class + update_wrapper(view, cls, updated=()) + + # and possible attributes set by decorators + # like csrf_exempt from dispatch + update_wrapper(view, cls.dispatch, assigned=()) + + # We need to set these on the view function, so that breadcrumb + # generation can pick out these bits of information from a + # resolved URL. + view.cls = cls + view.suffix = initkwargs.get('suffix', None) + return view + + def initialize_request(self, request, *args, **kargs): + """ + Set the `.action` attribute on the view, + depending on the request method. + """ + request = super(ViewSetMixin, self).initialize_request(request, *args, **kargs) + self.action = self.action_map.get(request.method.lower()) + return request + + def check_permissions(self, request, action:str=None, obj:object=None): + if action is None: + action = self.action + return super().check_permissions(request, action=action, obj=obj) + + +class ViewSet(ViewSetMixin, views.APIView): + """ + The base ViewSet class does not provide any actions by default. + """ + pass + + +class GenericViewSet(ViewSetMixin, generics.GenericAPIView): + """ + The GenericViewSet class does not provide any actions by default, + but does include the base set of generic view behavior, such as + the `get_object` and `get_queryset` methods. + """ + pass + + +class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, + mixins.ListModelMixin, + GenericViewSet): + """ + A viewset that provides default `list()` and `retrieve()` actions. + """ + pass + + +class ModelViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet): + """ + A viewset that provides default `create()`, `retrieve()`, `update()`, + `partial_update()`, `destroy()` and `list()` actions. + """ + pass + + +class ModelCrudViewSet(pagination.HeadersPaginationMixin, + pagination.ConditionalPaginationMixin, + ModelViewSet): + pass + + +class ModelListViewSet(pagination.HeadersPaginationMixin, + pagination.ConditionalPaginationMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + GenericViewSet): + pass diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py index 20f27a15..bf706df3 100644 --- a/taiga/base/decorators.py +++ b/taiga/base/decorators.py @@ -26,6 +26,7 @@ def detail_route(methods=['get'], **kwargs): def decorator(func): func.bind_to_methods = methods func.detail = True + func.permission_classes = kwargs.get('permission_classes', []) func.kwargs = kwargs return func return decorator @@ -38,6 +39,7 @@ def list_route(methods=['get'], **kwargs): def decorator(func): func.bind_to_methods = methods func.detail = False + func.permission_classes = kwargs.get('permission_classes', []) func.kwargs = kwargs return func return decorator @@ -52,6 +54,7 @@ def link(**kwargs): def decorator(func): func.bind_to_methods = ['get'] func.detail = True + func.permission_classes = kwargs.get('permission_classes', []) func.kwargs = kwargs return func return decorator @@ -66,6 +69,7 @@ def action(methods=['post'], **kwargs): def decorator(func): func.bind_to_methods = methods func.detail = True + func.permission_classes = kwargs.get('permission_classes', []) func.kwargs = kwargs return func return decorator diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 76960a4b..cd802eee 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -16,13 +16,14 @@ from django.db.models import Q +from django.db.models.sql.where import ExtraWhere, OR from rest_framework import filters from taiga.base import tags -class QueryParamsFilterMixin(object): +class QueryParamsFilterMixin(filters.BaseFilterBackend): _special_values_dict = { 'true': True, 'false': False, @@ -53,7 +54,8 @@ class QueryParamsFilterMixin(object): return queryset -class OrderByFilterMixin(object): + +class OrderByFilterMixin(QueryParamsFilterMixin): order_by_query_param = "order_by" def filter_queryset(self, request, queryset, view): @@ -72,18 +74,122 @@ class OrderByFilterMixin(object): if field_name not in order_by_fields: return queryset - return queryset.order_by(raw_fieldname) + return super().filter_queryset(request, queryset.order_by(raw_fieldname), view) -class FilterBackend(OrderByFilterMixin, - QueryParamsFilterMixin, - filters.BaseFilterBackend): +class FilterBackend(OrderByFilterMixin): """ Default filter backend. """ pass +class PermissionBasedFilterBackend(FilterBackend): + permission = None + + def filter_queryset(self, request, queryset, view): + # TODO: Make permissions aware of members permissions, now only check membership. + qs = queryset + + if request.user.is_authenticated() and request.user.is_superuser: + qs = qs + elif request.user.is_authenticated(): + qs = qs.filter(Q(project__owner=request.user) | + Q(project__members=request.user) | + Q(project__is_private=False)) + qs.query.where.add(ExtraWhere(["projects_project.public_permissions @> ARRAY['{}']".format(self.permission)], []), OR) + else: + qs = qs.filter(project__is_private=False) + qs.query.where.add(ExtraWhere(["projects_project.anon_permissions @> ARRAY['{}']".format(self.permission)], []), OR) + + return super().filter_queryset(request, qs.distinct(), view) + + +class CanViewProjectFilterBackend(PermissionBasedFilterBackend): + permission = "view_project" + + +class CanViewUsFilterBackend(PermissionBasedFilterBackend): + permission = "view_us" + + +class CanViewIssuesFilterBackend(PermissionBasedFilterBackend): + permission = "view_issues" + + +class CanViewTasksFilterBackend(PermissionBasedFilterBackend): + permission = "view_tasks" + + +class CanViewWikiPagesFilterBackend(PermissionBasedFilterBackend): + permission = "view_wiki_pages" + + +class CanViewWikiLinksFilterBackend(PermissionBasedFilterBackend): + permission = "view_wiki_links" + + +class CanViewMilestonesFilterBackend(PermissionBasedFilterBackend): + permission = "view_milestones" + +class PermissionBasedAttachmentFilterBackend(FilterBackend): + permission = None + + def filter_queryset(self, request, queryset, view): + # TODO: Make permissions aware of members permissions, now only check membership. + qs = queryset + + if request.user.is_authenticated() and request.user.is_superuser: + qs = qs + elif request.user.is_authenticated(): + qs = qs.filter(Q(project__owner=request.user) | + Q(project__members=request.user) | + Q(project__is_private=False)) + qs.query.where.add(ExtraWhere(["projects_project.public_permissions @> ARRAY['{}']".format(self.permission)], []), OR) + else: + qs = qs.filter(project__is_private=False) + qs.query.where.add(ExtraWhere(["projects_project.anon_permissions @> ARRAY['{}']".format(self.permission)], []), OR) + + ct = view.get_content_type() + qs = qs.filter(content_type=ct) + + return super().filter_queryset(request, qs.distinct(), view) + + +class CanViewUserStoryAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): + permission = "view_us" + + +class CanViewTaskAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): + permission = "view_tasks" + + +class CanViewIssueAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): + permission = "view_issues" + + +class CanViewWikiAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): + permission = "view_wiki_pages" + + +class CanViewProjectObjFilterBackend(FilterBackend): + def filter_queryset(self, request, queryset, view): + qs = queryset + + if request.user.is_authenticated() and request.user.is_superuser: + qs = qs + elif request.user.is_authenticated(): + qs = qs.filter(Q(owner=request.user) | + Q(members=request.user) | + Q(is_private=False)) + qs.query.where.add(ExtraWhere(["projects_project.public_permissions @> ARRAY['view_project']"], []), OR) + else: + qs = qs.filter(is_private=False) + qs.query.where.add(ExtraWhere(["projects_project.anon_permissions @> ARRAY['view_project']"], []), OR) + + return super().filter_queryset(request, qs.distinct(), view) + + class IsProjectMemberFilterBackend(FilterBackend): def filter_queryset(self, request, queryset, view): queryset = super().filter_queryset(request, queryset, view) @@ -92,7 +198,7 @@ class IsProjectMemberFilterBackend(FilterBackend): if user.is_authenticated(): queryset = queryset.filter(Q(project__members=request.user) | Q(project__owner=request.user)) - return queryset.distinct() + return super().filter_queryset(request, queryset.distinct(), view) class TagsFilter(FilterBackend): @@ -107,4 +213,4 @@ class TagsFilter(FilterBackend): if query_tags: queryset = tags.filter(queryset, contains=query_tags) - return queryset + return super().filter_queryset(request, queryset, view) diff --git a/taiga/base/serializers.py b/taiga/base/serializers.py index 5c0b9dde..34e14e29 100644 --- a/taiga/base/serializers.py +++ b/taiga/base/serializers.py @@ -45,6 +45,19 @@ class JsonField(serializers.WritableField): return data +class PgArrayField(serializers.WritableField): + """ + PgArray objects serializer. + """ + widget = widgets.Textarea + + def to_native(self, obj): + return obj + + def from_native(self, data): + return data + + class NeighborsSerializerMixin: def __init__(self, *args, **kwargs): diff --git a/taiga/base/utils/sequence.py b/taiga/base/utils/sequence.py new file mode 100644 index 00000000..c3d6ff5e --- /dev/null +++ b/taiga/base/utils/sequence.py @@ -0,0 +1,25 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +def first(iterable): + if len(iterable) == 0: + return None + return iterable[0] + +def next(data:list): + return data[1:] + diff --git a/taiga/permissions/__init__.py b/taiga/permissions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/permissions/models.py b/taiga/permissions/models.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py new file mode 100644 index 00000000..977e586f --- /dev/null +++ b/taiga/permissions/permissions.py @@ -0,0 +1,80 @@ +from django.utils.translation import ugettext_lazy as _ + +ANON_PERMISSIONS = [ + ('view_project', _('View project')), + ('view_milestones', _('View milestones')), + ('view_us', _('View user stories')), + ('view_tasks', _('View tasks')), + ('view_issues', _('View issues')), + ('view_wiki_pages', _('View wiki pages')), + ('view_wiki_links', _('View wiki links')), +] + +USER_PERMISSIONS = [ + ('view_project', _('View project')), + ('view_milestones', _('View milestones')), + ('view_us', _('View user stories')), + ('view_issues', _('View issues')), + ('vote_issues', _('Vote issues')), + ('view_tasks', _('View tasks')), + ('view_wiki_pages', _('View wiki pages')), + ('view_wiki_links', _('View wiki links')), + ('request_membership', _('Request membership')), + ('add_us_to_project', _('Add user story to project')), + ('add_comments_to_us', _('Add comments to user stories')), + ('add_comments_to_task', _('Add comments to tasks')), + ('add_issue', _('Add issues')), + ('add_comments_issue', _('Add comments to issues')), + ('add_wiki_page', _('Add wiki page')), + ('modify_wiki_page', _('Modify wiki page')), + ('add_wiki_link', _('Add wiki link')), + ('modify_wiki_link', _('Modify wiki link')), +] + +MEMBERS_PERMISSIONS = [ + ('view_project', _('View project')), + # Milestone permissions + ('view_milestones', _('View milestones')), + ('add_milestone', _('Add milestone')), + ('modify_milestone', _('Modify milestone')), + ('delete_last_milestone', _('Delete last milestone')), + ('delete_milestone', _('Delete milestone')), + ('add_us_to_milestone', _('Add use to milestone')), + ('remove_us_from_milestone', _('Remove us from milestone')), + ('reorder_us_on_milestone', _('Reorder us on milestone')), + # US permissions + ('view_us', _('View user story')), + ('add_us', _('Add user story')), + ('modify_us', _('Modify user story')), + ('delete_us', _('Delete user story')), + # Task permissions + ('view_tasks', _('View tasks')), + ('add_task', _('Add task')), + ('modify_task', _('Modify task')), + ('delete_task', _('Delete task')), + # Issue permissions + ('view_issues', _('View issues')), + ('vote_issues', _('Vote issues')), + ('add_issue', _('Add issue')), + ('modify_issue', _('Modify issue')), + ('delete_issue', _('Delete issue')), + # Wiki page permissions + ('view_wiki_pages', _('View wiki pages')), + ('add_wiki_page', _('Add wiki page')), + ('modify_wiki_page', _('Modify wiki page')), + ('delete_wiki_page', _('Delete wiki page')), + # Wiki link permissions + ('view_wiki_links', _('View wiki links')), + ('add_wiki_link', _('Add wiki link')), + ('modify_wiki_link', _('Modify wiki link')), + ('delete_wiki_link', _('Delete wiki link')), +] + +OWNERS_PERMISSIONS = [ + ('modify_project', _('Modify project')), + ('add_member', _('Add member')), + ('remove_member', _('Remove member')), + ('delete_project', _('Delete project')), + ('admin_project_values', _('Admin project values')), + ('admin_roles', _('Admin roles')), +] diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py new file mode 100644 index 00000000..3ca5ae8f --- /dev/null +++ b/taiga/permissions/service.py @@ -0,0 +1,75 @@ +from taiga.projects.models import Membership, Project +from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + + +def _get_user_project_membership(user, project): + if user.is_anonymous(): + return None + + try: + return Membership.objects.get(user=user, project=project) + except Membership.DoesNotExist: + return None + +def _get_object_project(obj): + project = None + + if isinstance(obj, Project): + project = obj + elif obj and hasattr(obj, 'project'): + project = obj.project + return project + + +def is_project_owner(user, obj): + if user.is_superuser: + return True + + project = _get_object_project(obj) + + if project: + return project.owner == user + return False + + +def user_has_perm(user, perm, obj=None): + project = _get_object_project(obj) + + if not project: + return False + + return perm in get_user_project_permissions(user, project) + + +def role_has_perm(role, perm): + return perm in role.permissions + + +def _get_membership_permissions(membership): + if membership and membership.role and membership.role.permissions: + return membership.role.permissions + return [] + + +def get_user_project_permissions(user, project): + membership = _get_user_project_membership(user, project) + if user.is_superuser: + owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS)) + members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) + anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) + public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS)) + return set(owner_permissions + members_permissions + public_permissions + anon_permissions) + elif project.owner == user: + owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS)) + members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) + return set(project.anon_permissions + project.public_permissions + members_permissions + owner_permissions) + elif membership: + if membership.is_owner: + owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS)) + return set(project.anon_permissions + project.public_permissions + _get_membership_permissions(membership) + owner_permissions) + else: + return set(project.anon_permissions + project.public_permissions + _get_membership_permissions(membership)) + elif user.is_authenticated(): + return set(project.anon_permissions + project.public_permissions) + else: + return set(project.anon_permissions) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index d2342f55..a9bf87f2 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -20,7 +20,6 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 -from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework.exceptions import ParseError from rest_framework import viewsets @@ -30,9 +29,11 @@ from djmail.template_mail import MagicMailBuilder from taiga.base import filters from taiga.base import exceptions as exc -from taiga.base.decorators import list_route, detail_route -from taiga.base.permissions import has_project_perm -from taiga.base.api import ModelCrudViewSet, ModelListViewSet, RetrieveModelMixin +from taiga.base.decorators import list_route +from taiga.base.decorators import detail_route +from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.mixins import RetrieveModelMixin +from taiga.base.api.permissions import IsAuthenticatedPermission, AllowAnyPermission from taiga.base.utils.slug import slugify_uniquely from taiga.users.models import Role @@ -40,20 +41,95 @@ from . import serializers from . import models from . import permissions from . import services + from .votes.utils import attach_votescount_to_queryset from .votes import services as votes_service from .votes import serializers as votes_serializers -class ProjectAdminViewSet(ModelCrudViewSet): +class ProjectViewSet(ModelCrudViewSet): serializer_class = serializers.ProjectDetailSerializer list_serializer_class = serializers.ProjectSerializer - permission_classes = (IsAuthenticated, permissions.ProjectAdminPermission) + permission_classes = (permissions.ProjectPermission, ) + filter_backends = (filters.CanViewProjectObjFilterBackend,) def get_queryset(self): qs = models.Project.objects.all() - qs = attach_votescount_to_queryset(qs, as_field="stars_count") - return qs + return attach_votescount_to_queryset(qs, as_field="stars_count") + + @detail_route(methods=['get']) + def stats(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'stats', project) + return Response(services.get_stats_for_project(project)) + + @detail_route(methods=['post']) + def star(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'star', project) + votes_service.add_vote(project, user=request.user) + return Response(status=status.HTTP_200_OK) + + @detail_route(methods=['post']) + def unstar(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'unstar', project) + votes_service.remove_vote(project, user=request.user) + return Response(status=status.HTTP_200_OK) + + @detail_route(methods=['get']) + def issues_stats(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'issues_stats', project) + return Response(services.get_stats_for_project_issues(project)) + + @detail_route(methods=['get']) + def issue_filters_data(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'issues_filters_data', project) + return Response(services.get_issues_filters_data(project)) + + @detail_route(methods=['get']) + def tags(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'tags', project) + return Response(services.get_all_tags(project)) + + @detail_route(methods=['get']) + def fans(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'fans', project) + + voters = votes_service.get_voters(project) + voters_data = votes_serializers.VoterSerializer(voters, many=True) + return Response(voters_data.data) + + @detail_route(methods=["POST"]) + def create_template(self, request, **kwargs): + template_name = request.DATA.get('template_name', None) + template_description = request.DATA.get('template_description', None) + + if not template_name: + raise ParseError("Not valid template name") + + if not template_description: + raise ParseError("Not valid template description") + + template_slug = slugify_uniquely(template_name, models.ProjectTemplate) + + project = self.get_object() + + self.check_permissions(request, 'create_template', project) + + template = models.ProjectTemplate( + name=template_name, + slug=template_slug, + description=template_description, + ) + + template.load_data_from_project(project) + template.save() + return Response(serializers.ProjectTemplateSerializer(template).data, status=201) def pre_save(self, obj): if not obj.id: @@ -66,61 +142,11 @@ class ProjectAdminViewSet(ModelCrudViewSet): super().pre_save(obj) -class ProjectViewSet(ModelCrudViewSet): - serializer_class = serializers.ProjectDetailSerializer - list_serializer_class = serializers.ProjectSerializer - permission_classes = (IsAuthenticated, permissions.ProjectPermission) - - def get_queryset(self): - qs = models.Project.objects.all() - qs = attach_votescount_to_queryset(qs, as_field="stars_count") - qs = qs.filter(Q(owner=self.request.user) | - Q(members=self.request.user)) - return qs.distinct() - - @detail_route(methods=['get']) - def stats(self, request, pk=None): - project = self.get_object() - return Response(services.get_stats_for_project(project)) - - @detail_route(methods=['post'], permission_classes=(IsAuthenticated,)) - def star(self, request, pk=None): - project = self.get_object() - votes_service.add_vote(project, user=request.user) - return Response(status=status.HTTP_200_OK) - - @detail_route(methods=['post'], permission_classes=(IsAuthenticated,)) - def unstar(self, request, pk=None): - project = self.get_object() - votes_service.remove_vote(project, user=request.user) - return Response(status=status.HTTP_200_OK) - - @detail_route(methods=['get']) - def issues_stats(self, request, pk=None): - project = self.get_object() - return Response(services.get_stats_for_project_issues(project)) - - @detail_route(methods=['get']) - def issue_filters_data(self, request, pk=None): - project = self.get_object() - return Response(services.get_issues_filters_data(project)) - - @detail_route(methods=['get']) - def tags(self, request, pk=None): - project = self.get_object() - return Response(services.get_all_tags(project)) - - def pre_save(self, obj): - if not obj.id: - obj.owner = self.request.user - - super().pre_save(obj) - - class MembershipViewSet(ModelCrudViewSet): model = models.Membership serializer_class = serializers.MembershipSerializer - permission_classes = (IsAuthenticated, permissions.MembershipPermission) + permission_classes = (permissions.MembershipPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project", "role") def create(self, request, *args, **kwargs): @@ -164,14 +190,14 @@ class MembershipViewSet(ModelCrudViewSet): email.send() -class InvitationViewSet(RetrieveModelMixin, viewsets.ReadOnlyModelViewSet): +class InvitationViewSet(ModelListViewSet): """ Only used by front for get invitation by it token. """ queryset = models.Membership.objects.filter(user__isnull=True) serializer_class = serializers.MembershipSerializer lookup_field = "token" - permission_classes = (AllowAny,) + permission_classes = (AllowAnyPermission,) def list(self, *args, **kwargs): raise exc.PermissionDenied(_("You don't have permisions to see that.")) @@ -180,13 +206,10 @@ class InvitationViewSet(RetrieveModelMixin, viewsets.ReadOnlyModelViewSet): class RolesViewSet(ModelCrudViewSet): model = Role serializer_class = serializers.RoleSerializer - permission_classes = (IsAuthenticated, permissions.RolesPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.RolesPermission, ) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) - def get_queryset(self): - return self.model.objects.all().prefetch_related('permissions') - # User Stories commin ViewSets @@ -213,139 +236,93 @@ class BulkUpdateOrderMixin(object): project = get_object_or_404(models.Project, id=project_id) - if request.user != project.owner and not has_project_perm(request.user, project, self.bulk_update_perm): - raise exc.PermissionDenied(_("You don't have permisions %s.") % self.bulk_update_perm) + self.check_permissions(request, 'bulk_update_order', project) - self.bulk_update_order(project, request.user, bulk_data) + self.__class__.bulk_update_order_action(project, request.user, bulk_data) return Response(data=None, status=status.HTTP_204_NO_CONTENT) class PointsViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.Points serializer_class = serializers.PointsSerializer - permission_classes = (IsAuthenticated, permissions.PointsPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.PointsPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) bulk_update_param = "bulk_points" bulk_update_perm = "change_points" - bulk_update_order = services.bulk_update_points_order + bulk_update_order_action = services.bulk_update_points_order class UserStoryStatusViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.UserStoryStatus serializer_class = serializers.UserStoryStatusSerializer - permission_classes = (IsAuthenticated, permissions.UserStoryStatusPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.UserStoryStatusPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) bulk_update_param = "bulk_userstory_statuses" bulk_update_perm = "change_userstorystatus" - bulk_update_order = services.bulk_update_userstory_status_order + bulk_update_order_action = services.bulk_update_userstory_status_order class TaskStatusViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.TaskStatus serializer_class = serializers.TaskStatusSerializer - permission_classes = (IsAuthenticated, permissions.TaskStatusPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.TaskStatusPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) bulk_update_param = "bulk_task_statuses" bulk_update_perm = "change_taskstatus" - bulk_update_order = services.bulk_update_task_status_order + bulk_update_order_action = services.bulk_update_task_status_order class SeverityViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.Severity serializer_class = serializers.SeveritySerializer - permission_classes = (IsAuthenticated, permissions.SeverityPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.SeverityPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) bulk_update_param = "bulk_severities" bulk_update_perm = "change_severity" - bulk_update_order = services.bulk_update_severity_order + bulk_update_order_action = services.bulk_update_severity_order class PriorityViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.Priority serializer_class = serializers.PrioritySerializer - permission_classes = (IsAuthenticated, permissions.PriorityPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.PriorityPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) bulk_update_param = "bulk_priorities" bulk_update_perm = "change_priority" - bulk_update_order = services.bulk_update_priority_order + bulk_update_order_action = services.bulk_update_priority_order class IssueTypeViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.IssueType serializer_class = serializers.IssueTypeSerializer - permission_classes = (IsAuthenticated, permissions.IssueTypePermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.IssueTypePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) bulk_update_param = "bulk_issue_types" bulk_update_perm = "change_issuetype" - bulk_update_order = services.bulk_update_issue_type_order + bulk_update_order_action = services.bulk_update_issue_type_order class IssueStatusViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): model = models.IssueStatus serializer_class = serializers.IssueStatusSerializer - permission_classes = (IsAuthenticated, permissions.IssueStatusPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.IssueStatusPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) bulk_update_param = "bulk_issue_statuses" bulk_update_perm = "change_issuestatus" - bulk_update_order = services.bulk_update_issue_status_order + bulk_update_order_action = services.bulk_update_issue_status_order class ProjectTemplateViewSet(ModelCrudViewSet): model = models.ProjectTemplate serializer_class = serializers.ProjectTemplateSerializer - permission_classes = (IsAuthenticated, permissions.ProjectTemplatePermission) - - @list_route(methods=["POST"]) - def create_from_project(self, request, **kwargs): - project_id = request.DATA.get('project_id', None) - template_name = request.DATA.get('template_name', None) - template_description = request.DATA.get('template_description', None) - - if not template_name: - raise ParseError("Not valid template name") - - template_slug = slugify_uniquely(template_name, models.ProjectTemplate) - - try: - project = models.Project.objects.get(pk=project_id) - except models.Project.DoesNotExist: - raise ParseError("Not valid project_id") - - template = models.ProjectTemplate( - name=template_name, - slug=template_slug, - description=template_description, - ) - - template.load_data_from_project(project) - template.save() - return Response(self.serializer_class(template).data, status=201) + permission_classes = (permissions.ProjectTemplatePermission,) def get_queryset(self): return models.ProjectTemplate.objects.all() - - -class FansViewSet(ModelCrudViewSet): - serializer_class = votes_serializers.VoterSerializer - list_serializer_class = votes_serializers.VoterSerializer - permission_classes = (IsAuthenticated,) - - def get_queryset(self): - project = models.Project.objects.get(pk=self.kwargs.get("project_id")) - return votes_service.get_voters(project) - - -class StarredViewSet(ModelCrudViewSet): - serializer_class = serializers.StarredSerializer - list_serializer_class = serializers.StarredSerializer - permission_classes = (IsAuthenticated,) - - def get_queryset(self): - return votes_service.get_voted(self.kwargs.get("user_id"), model=models.Project) diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index e9b9f79e..d8bdf21f 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -14,15 +14,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import os + from django.utils.translation import ugettext as _ from django.contrib.contenttypes.models import ContentType from django.shortcuts import get_object_or_404 -from rest_framework.permissions import IsAuthenticated +from django.conf import settings +from django import http from taiga.base.api import ModelCrudViewSet +from taiga.base.api import generics from taiga.base import filters from taiga.base import exceptions as exc -from taiga.projects.history.services import take_snapshot from taiga.projects.notifications import WatchedResourceMixin from taiga.projects.history import HistoryResourceMixin @@ -36,9 +39,6 @@ from . import models class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): model = models.Attachment serializer_class = serializers.AttachmentSerializer - permission_classes = (IsAuthenticated, permissions.AttachmentPermission,) - - filter_backends = (filters.IsProjectMemberFilterBackend,) filter_fields = ["project", "object_id"] content_type = None @@ -47,12 +47,6 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCru app_name, model = self.content_type.split(".", 1) return get_object_or_404(ContentType, app_label=app_name, model=model) - def get_queryset(self): - ct = self.get_content_type() - qs = super().get_queryset() - qs = qs.filter(content_type=ct) - return qs.distinct() - def pre_save(self, obj): if not obj.id: obj.content_type = self.get_content_type() @@ -60,19 +54,13 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCru super().pre_save(obj) - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) - - if (obj.project.owner != self.request.user and - obj.project.memberships.filter(user=self.request.user).count() == 0): - raise exc.PermissionDenied(_("You don't have permissions for " - "add attachments to this user story")) - def get_object_for_snapshot(self, obj): return obj.content_object class UserStoryAttachmentViewSet(BaseAttachmentViewSet): + permission_classes = (permissions.UserStoryAttachmentPermission,) + filter_backends = (filters.CanViewUserStoryAttachmentFilterBackend,) content_type = "userstories.userstory" create_notification_template = "create_userstory_notification" update_notification_template = "update_userstory_notification" @@ -80,6 +68,8 @@ class UserStoryAttachmentViewSet(BaseAttachmentViewSet): class IssueAttachmentViewSet(BaseAttachmentViewSet): + permission_classes = (permissions.IssueAttachmentPermission,) + filter_backends = (filters.CanViewIssueAttachmentFilterBackend,) content_type = "issues.issue" create_notification_template = "create_issue_notification" update_notification_template = "update_issue_notification" @@ -87,6 +77,8 @@ class IssueAttachmentViewSet(BaseAttachmentViewSet): class TaskAttachmentViewSet(BaseAttachmentViewSet): + permission_classes = (permissions.TaskAttachmentPermission,) + filter_backends = (filters.CanViewTaskAttachmentFilterBackend,) content_type = "tasks.task" create_notification_template = "create_task_notification" update_notification_template = "update_task_notification" @@ -94,7 +86,31 @@ class TaskAttachmentViewSet(BaseAttachmentViewSet): class WikiAttachmentViewSet(BaseAttachmentViewSet): + permission_classes = (permissions.WikiAttachmentPermission,) + filter_backends = (filters.CanViewWikiAttachmentFilterBackend,) content_type = "wiki.wikipage" create_notification_template = "create_wiki_notification" update_notification_template = "update_wiki_notification" destroy_notification_template = "destroy_wiki_notification" + + +class RawAttachmentView(generics.RetrieveAPIView): + queryset = models.Attachment.objects.all() + permission_classes = (permissions.RawAttachmentPermission,) + + def _serve_attachment(self, attachment): + if settings.IN_DEVELOPMENT_SERVER: + return http.HttpResponseRedirect(attachment.url) + + name = attachment.name + response = http.HttpResponse() + response['X-Accel-Redirect'] = "/{filepath}".format(filepath=name) + response['Content-Disposition'] = 'attachment;filename={filename}'.format( + filename=os.path.basename(name)) + + return response + + def retrieve(self, request, *args, **kwargs): + self.object = self.get_object() + self.check_permissions(request, 'retrieve', self.object) + return self._serve_attachment(self.object.attached_file) diff --git a/taiga/projects/attachments/permissions.py b/taiga/projects/attachments/permissions.py index a28ac3be..938cc63c 100644 --- a/taiga/projects/attachments/permissions.py +++ b/taiga/projects/attachments/permissions.py @@ -15,14 +15,62 @@ # along with this program. If not, see . -from taiga.base.permissions import BasePermission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, AllowAny, PermissionComponent) -class AttachmentPermission(BasePermission): - get_permission = "view_attachment" - post_permission = "add_attachment" - put_permission = "change_attachment" - patch_permission = "change_attachment" - delete_permission = "delete_attachment" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class IsAttachmentOwnerPerm(PermissionComponent): + def check_permissions(self, request, view, obj=None): + if obj and obj.owner and request.user.is_authenticated(): + return request.user == obj.owner + return False + + +class UserStoryAttachmentPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_us') | IsAttachmentOwnerPerm() + create_perms = HasProjectPerm('modify_us') + update_perms = HasProjectPerm('modify_us') | IsAttachmentOwnerPerm() + destroy_perms = HasProjectPerm('modify_us') | IsAttachmentOwnerPerm() + list_perms = AllowAny() + + +class TaskAttachmentPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_tasks') | IsAttachmentOwnerPerm() + create_perms = HasProjectPerm('modify_task') + update_perms = HasProjectPerm('modify_task') | IsAttachmentOwnerPerm() + destroy_perms = HasProjectPerm('modify_task') | IsAttachmentOwnerPerm() + list_perms = AllowAny() + + +class IssueAttachmentPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_issues') | IsAttachmentOwnerPerm() + create_perms = HasProjectPerm('modify_issue') + update_perms = HasProjectPerm('modify_issue') | IsAttachmentOwnerPerm() + destroy_perms = HasProjectPerm('modify_issue') | IsAttachmentOwnerPerm() + list_perms = AllowAny() + + +class WikiAttachmentPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_wiki_pages') | IsAttachmentOwnerPerm() + create_perms = HasProjectPerm('modify_wiki_page') + update_perms = HasProjectPerm('modify_wiki_page') | IsAttachmentOwnerPerm() + destroy_perms = HasProjectPerm('modify_wiki_page') | IsAttachmentOwnerPerm() + list_perms = AllowAny() + + +class RawAttachmentPerm(PermissionComponent): + def check_permissions(self, request, view, obj=None): + is_owner = IsAttachmentOwnerPerm().check_permissions(request, view, obj) + if obj.content_type.app_label == "userstories" and obj.content_type.model == "userstory": + return UserStoryAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner + elif obj.content_type.app_label == "tasks" and obj.content_type.model == "task": + return TaskAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner + elif obj.content_type.app_label == "issues" and obj.content_type.model == "issue": + return IssueAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner + elif obj.content_type.app_label == "wiki" and obj.content_type.model == "wikipage": + return WikiAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner + return False + + +class RawAttachmentPermission(ResourcePermission): + retrieve_perms = RawAttachmentPerm() diff --git a/taiga/projects/attachments/views.py b/taiga/projects/attachments/views.py deleted file mode 100644 index 567944ba..00000000 --- a/taiga/projects/attachments/views.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -from django.conf import settings -from django import http - -from rest_framework import generics -from rest_framework.permissions import IsAuthenticated - -from . import permissions -from . import models - - -def serve_attachment(request, attachment): - if settings.IN_DEVELOPMENT_SERVER: - return http.HttpResponseRedirect(attachment.url) - - name = attachment.name - response = http.HttpResponse() - response['X-Accel-Redirect'] = "/{filepath}".format(filepath=name) - response['Content-Disposition'] = 'attachment;filename={filename}'.format( - filename=os.path.basename(name)) - - return response - - -class RawAttachmentView(generics.RetrieveAPIView): - queryset = models.Attachment.objects.all() - permission_classes = (IsAuthenticated, permissions.AttachmentPermission,) - - def retrieve(self, request, *args, **kwargs): - self.object = self.get_object() - return serve_attachment(request, self.object.attached_file) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index 8aa800d6..7b77e02f 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -31,8 +31,6 @@ from . import services # TODO: add specific permission for view history? class HistoryViewSet(GenericViewSet): - filter_backends = (IsProjectMemberFilterBackend,) - permission_classes = (IsAuthenticated, permissions.HistoryPermission) serializer_class = serializers.HistoryEntrySerializer content_type = None @@ -41,13 +39,13 @@ class HistoryViewSet(GenericViewSet): app_name, model = self.content_type.split(".", 1) return get_object_or_404(ContentType, app_label=app_name, model=model) - def get_object(self): + def get_queryset(self): ct = self.get_content_type() model_cls = ct.model_class() qs = model_cls.objects.all() filtered_qs = self.filter_queryset(qs) - return super().get_object(queryset=filtered_qs) + return filtered_qs def response_for_queryset(self, queryset): # Switch between paginated or standard style responses @@ -66,21 +64,27 @@ class HistoryViewSet(GenericViewSet): def retrieve(self, request, pk): obj = self.get_object() + self.check_permissions(request, "retrieve", obj) + qs = services.get_history_queryset_by_model_instance(obj) return self.response_for_queryset(qs) class UserStoryHistory(HistoryViewSet): content_type = "userstories.userstory" + permission_classes = (permissions.UserStoryHistoryPermission,) class TaskHistory(HistoryViewSet): content_type = "tasks.task" + permission_classes = (permissions.TaskHistoryPermission,) class IssueHistory(HistoryViewSet): content_type = "issues.issue" + permission_classes = (permissions.IssueHistoryPermission,) class WikiHistory(HistoryViewSet): - content_type = "wiki.wiki" + content_type = "wiki.wikipage" + permission_classes = (permissions.WikiHistoryPermission,) diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index 888f8095..7977724d 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -1,4 +1,6 @@ # Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -12,9 +14,21 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.permissions import Permission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, AllowAny) -class HistoryPermission(Permission): - def has_object_permission(self, request, view, obj): - # TODO: change this. - return True + +class UserStoryHistoryPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + + +class TaskHistoryPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + + +class IssueHistoryPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + + +class WikiHistoryPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 6f250553..c2b605af 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -15,19 +15,21 @@ # along with this program. If not, see . from django.utils.translation import ugettext_lazy as _ +from django.shortcuts import get_object_or_404 from django.db.models import Q +from django.http import Http404 -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework import status -from rest_framework import filters from taiga.base import filters from taiga.base import exceptions as exc -from taiga.base.decorators import list_route, detail_route -from taiga.base.api import ModelCrudViewSet +from taiga.base.decorators import detail_route +from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base import tags +from taiga.users.models import User + from taiga.projects.notifications import WatchedResourceMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.history import HistoryResourceMixin @@ -42,7 +44,7 @@ from . import serializers class IssuesFilter(filters.FilterBackend): - filter_fields = ( "status", "severity", "priority", "owner", "assigned_to", "tags", "type") + filter_fields = ("status", "severity", "priority", "owner", "assigned_to", "tags", "type") _special_values_dict = { 'true': True, 'false': False, @@ -80,7 +82,7 @@ class IssuesFilter(filters.FilterBackend): for name, value in filter(lambda x: x[0] != "tags", filterdata.items()): if None in value: - qs_in_kwargs = {"{0}__in".format(name): [v for v in value if v != None]} + qs_in_kwargs = {"{0}__in".format(name): [v for v in value if v is not None]} qs_isnull_kwargs = {"{0}__isnull".format(name): True} queryset = queryset.filter(Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs)) else: @@ -104,9 +106,9 @@ class IssuesOrdering(filters.FilterBackend): class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): serializer_class = serializers.IssueNeighborsSerializer list_serializer_class = serializers.IssueSerializer - permission_classes = (IsAuthenticated, permissions.IssuePermission) + permission_classes = (permissions.IssuePermission, ) - filter_backends = (filters.IsProjectMemberFilterBackend, IssuesFilter, IssuesOrdering) + filter_backends = (filters.CanViewIssuesFilterBackend, IssuesFilter, IssuesOrdering) retrieve_exclude_filters = (IssuesFilter,) filter_fields = ("project",) @@ -133,42 +135,65 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, def pre_conditions_on_save(self, obj): super().pre_conditions_on_save(obj) - if (obj.project.owner != self.request.user and - obj.project.memberships.filter(user=self.request.user).count() == 0): - raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) - if obj.milestone and obj.milestone.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this milestone to this issue.")) if obj.status and obj.status.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this status to this issue.")) if obj.severity and obj.severity.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this severity to this issue.")) if obj.priority and obj.priority.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this priority to this issue.")) if obj.type and obj.type.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions for add/modify this issue.")) + raise exc.PermissionDenied(_("You don't have permissions to set this type to this issue.")) - @detail_route(methods=['post'], permission_classes=(IsAuthenticated,)) + @detail_route(methods=['post']) def upvote(self, request, pk=None): - issue = self.get_object() + issue = get_object_or_404(models.Issue, pk=pk) + + self.check_permissions(request, 'upvote', issue) + votes_service.add_vote(issue, user=request.user) return Response(status=status.HTTP_200_OK) - @detail_route(methods=['post'], permission_classes=(IsAuthenticated,)) + @detail_route(methods=['post']) def downvote(self, request, pk=None): - issue = self.get_object() + issue = get_object_or_404(models.Issue, pk=pk) + + self.check_permissions(request, 'downvote', issue) + votes_service.remove_vote(issue, user=request.user) return Response(status=status.HTTP_200_OK) -class VotersViewSet(ModelCrudViewSet): +class VotersViewSet(ModelListViewSet): serializer_class = votes_serializers.VoterSerializer list_serializer_class = votes_serializers.VoterSerializer - permission_classes = (IsAuthenticated,) + permission_classes = (permissions.IssueVotersPermission, ) + + def retrieve(self, request, *args, **kwargs): + pk = kwargs.get("pk", None) + issue_id = kwargs.get("issue_id", None) + issue = get_object_or_404(models.Issue, pk=issue_id) + + self.check_permissions(request, 'retrieve', issue) + + try: + self.object = votes_service.get_voters(issue).get(pk=pk) + except User.DoesNotExist: + raise Http404 + + serializer = self.get_serializer(self.object) + return Response(serializer.data) + + def list(self, request, *args, **kwargs): + issue_id = kwargs.get("issue_id", None) + issue = get_object_or_404(models.Issue, pk=issue_id) + self.check_permissions(request, 'list', issue) + return super().list(request, *args, **kwargs) def get_queryset(self): issue = models.Issue.objects.get(pk=self.kwargs.get("issue_id")) diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index 05efcb6f..b1fd68b1 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -15,14 +15,36 @@ # along with this program. If not, see . -from taiga.base.permissions import BasePermission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, PermissionComponent, + AllowAny, IsAuthenticated) -class IssuePermission(BasePermission): - get_permission = "view_issue" - post_permission = "add_issue" - put_permission = "change_issue" - patch_permission = "change_issue" - delete_permission = "delete_issue" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class IssuePermission(ResourcePermission): + enought_perms = IsProjectOwner() + global_perms = None + retrieve_perms = HasProjectPerm('view_issues') + create_perms = HasProjectPerm('add_issue') + update_perms = HasProjectPerm('modify_issue') + destroy_perms = HasProjectPerm('delete_issue') + list_perms = AllowAny() + upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') + downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') + + +class HasIssueIdUrlParam(PermissionComponent): + def check_permissions(self, request, view, obj=None): + param = view.kwargs.get('issue_id', None) + if param: + return True + return False + + +class IssueVotersPermission(ResourcePermission): + enought_perms = IsProjectOwner() + global_perms = None + retrieve_perms = HasProjectPerm('view_issues') + create_perms = HasProjectPerm('add_issue') + update_perms = HasProjectPerm('modify_issue') + destroy_perms = HasProjectPerm('delete_issue') + list_perms = HasProjectPerm('view_issues') diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 6a145e13..501af3bb 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -38,8 +38,8 @@ import datetime class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): serializer_class = serializers.MilestoneSerializer - permission_classes = (IsAuthenticated, permissions.MilestonePermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.MilestonePermission,) + filter_backends = (filters.CanViewMilestonesFilterBackend,) filter_fields = ("project",) def get_queryset(self): @@ -51,13 +51,6 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView qs = qs.order_by("-estimated_start") return qs - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) - - if (obj.project.owner != self.request.user and - obj.project.memberships.filter(user=self.request.user).count() == 0): - raise exc.PreconditionError(_("You must not add a new milestone to this project.")) - def pre_save(self, obj): if not obj.id: obj.owner = self.request.user @@ -67,6 +60,9 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView @detail_route(methods=['get']) def stats(self, request, pk=None): milestone = get_object_or_404(models.Milestone, pk=pk) + + self.check_permissions(request, "stats", milestone) + total_points = milestone.total_points milestone_stats = { 'name': milestone.name, diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py index 1ad1ec5f..d039815a 100644 --- a/taiga/projects/milestones/permissions.py +++ b/taiga/projects/milestones/permissions.py @@ -14,15 +14,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.permissions import BasePermission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, AllowAny) -class MilestonePermission(BasePermission): - get_permission = "view_milestone" - post_permission = "add_milestone" - put_permission = "change_milestone" - patch_permission = "change_milestone" - delete_permission = "delete_milestone" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] - +class MilestonePermission(ResourcePermission): + enought_perms = IsProjectOwner() + global_perms = None + retrieve_perms = HasProjectPerm('view_milestones') + create_perms = HasProjectPerm('add_milestone') + update_perms = HasProjectPerm('modify_milestone') + destroy_perms = HasProjectPerm('delete_milestone') + list_perms = AllowAny() + stats_perms = HasProjectPerm('view_milestones') diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index d9154101..652455ce 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -52,9 +52,9 @@ class MilestoneSerializer(serializers.ModelSerializer): qs = None # If the milestone exists: if self.object and attrs.get("name", None): - qs = models.Milestone.objects.filter(project=self.object.project, name=attrs[source]) + qs = models.Milestone.objects.filter(project=self.object.project, name=attrs[source]).exclude(pk=self.object.pk) - if not self.object and attrs.get("project", None) and attrs.get("name", None): + if not self.object and attrs.get("project", None) and attrs.get("name", None): qs = models.Milestone.objects.filter(project=attrs["project"], name=attrs[source]) if qs and qs.exists(): diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 8c511215..e12474cb 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -23,12 +23,13 @@ from django.db.models.loading import get_model from django.conf import settings from django.dispatch import receiver from django.contrib.auth import get_user_model -from django.contrib.auth.models import Permission from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from picklefield.fields import PickledObjectField from django_pgjson.fields import JsonField +from djorm_pgarray.fields import TextArrayField +from taiga.permissions.permissions import ANON_PERMISSIONS, USER_PERMISSIONS from taiga.base.tags import TaggedMixin from taiga.users.models import Role @@ -43,6 +44,7 @@ VIDEOCONFERENCES_CHOICES = ( ('talky', 'Talky'), ) + class Membership(models.Model): # This model stores all project memberships. Also # stores invitations to memberships that does not have @@ -152,6 +154,16 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): related_name="projects", null=True, blank=True, default=None, verbose_name=_("creation template")) + anon_permissions = TextArrayField(blank=True, null=True, + default=[], + verbose_name=_("anonymous permissions"), + choices=ANON_PERMISSIONS) + public_permissions = TextArrayField(blank=True, null=True, + default=[], + verbose_name=_("user permissions"), + choices=USER_PERMISSIONS) + is_private = models.BooleanField(default=False, null=False, blank=True, + verbose_name=_("is private")) class Meta: verbose_name = "project" @@ -193,7 +205,10 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): return # Get point instance that represent a null/undefined - null_points_value = self.points.get(value=None) + try: + null_points_value = self.points.get(value=None) + except Points.DoesNotExist: + null_points_value = None # Iter over all project user stories and create # role point instance for new created roles. @@ -513,7 +528,6 @@ class ProjectTemplate(models.Model): "severity": getattr(project.default_severity, "name", None) } - self.us_statuses = [] for us_status in project.us_statuses.all(): self.us_statuses.append({ @@ -576,17 +590,19 @@ class ProjectTemplate(models.Model): self.roles = [] for role in project.roles.all(): - permissions = [p.codename for p in role.permissions.all()] self.roles.append({ "name": role.name, "slug": role.slug, - "permissions": permissions, + "permissions": role.permissions, "order": role.order, "computable": role.computable }) - owner_membership = Membership.objects.get(project=project, user=project.owner) - self.default_owner_role = owner_membership.role.slug + try: + owner_membership = Membership.objects.get(project=project, user=project.owner) + self.default_owner_role = owner_membership.role.slug + except Membership.DoesNotExist: + self.default_owner_role = self.roles[0].get("slug", None) def apply_to_project(self, project): if project.id is None: @@ -661,16 +677,14 @@ class ProjectTemplate(models.Model): ) for role in self.roles: - newRoleInstance = Role.objects.create( + Role.objects.create( name=role["name"], slug=role["slug"], order=role["order"], computable=role["computable"], - project=project + project=project, + permissions=role['permissions'] ) - permissions = [Permission.objects.get(codename=codename) for codename in role["permissions"]] - for permission in permissions: - newRoleInstance.permissions.add(permission) if self.points: project.default_points = Points.objects.get(name=self.default_options["points"], @@ -696,7 +710,6 @@ class ProjectTemplate(models.Model): if self.severities: project.default_severity = Severity.objects.get(name=self.default_options["severity"], project=project) - if self.default_owner_role: # FIXME: is operation should to be on template apply method? Membership.objects.create(user=project.owner, diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index de20c845..5a5e4272 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -14,129 +14,117 @@ # along with this program. If not, see . -from taiga.base.permissions import BasePermission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsAuthenticated, IsProjectOwner, + AllowAny, IsSuperUser) -class ProjectPermission(BasePermission): - get_permission = "view_project" - post_permission = None - put_permission = "change_project" - patch_permission = "change_project" - delete_permission = None - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = [] +class ProjectPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsAuthenticated() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + stats_perms = AllowAny() + star_perms = IsAuthenticated() + unstar_perms = IsAuthenticated() + issues_stats_perms = AllowAny() + issues_filters_data_perms = AllowAny() + tags_perms = AllowAny() + fans_perms = HasProjectPerm('view_project') + create_template_perms = IsSuperUser() -class ProjectAdminPermission(BasePermission): - def has_permission(self, request, view): - if request.method in self.safe_methods: - return True - return super().has_permission(request, view) - - def has_object_permission(self, request, view, obj): - if request.method in self.safe_methods: - return True - return super().has_object_permission(request, view, obj) - - -class MembershipPermission(BasePermission): - get_permission = "view_membership" - post_permission = "add_membership" - put_permission = "change_membership" - patch_permission = "change_membership" - delete_permission = "delete_membership" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class MembershipPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() # User Stories -class PointsPermission(BasePermission): - get_permission = "view_points" - post_permission = "add_points" - put_permission = "change_points" - patch_permission = "change_points" - delete_permission = "delete_points" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class PointsPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() -class UserStoryStatusPermission(BasePermission): - get_permission = "view_userstorystatus" - post_permission = "add_userstorystatus" - put_permission = "change_userstorystatus" - patch_permission = "change_userstorystatus" - delete_permission = "delete_userstorystatus" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class UserStoryStatusPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() # Tasks -class TaskStatusPermission(BasePermission): - get_permission = "view_taskstatus" - post_permission = "ade_taskstatus" - put_permission = "change_taskstatus" - patch_permission = "change_taskstatus" - delete_permission = "delete_taskstatus" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class TaskStatusPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() # Issues -class SeverityPermission(BasePermission): - get_permission = "view_severity" - post_permission = "add_severity" - put_permission = "change_severity" - patch_permission = "change_severity" - delete_permission = "delete_severity" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class SeverityPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() -class PriorityPermission(BasePermission): - get_permission = "view_priority" - post_permission = "add_priority" - put_permission = "change_priority" - patch_permission = "change_priority" - delete_permission = "delete_priority" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class PriorityPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() -class IssueStatusPermission(BasePermission): - get_permission = "view_issuestatus" - post_permission = "add_issuestatus" - put_permission = "change_issuestatus" - patch_permission = "change_issuestatus" - delete_permission = "delete_issuestatus" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class IssueStatusPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() -class IssueTypePermission(BasePermission): - get_permission = "view_issuetype" - post_permission = "add_issuetype" - put_permission = "change_issuetype" - patch_permission = "change_issuetype" - delete_permission = "delete_issuetype" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class IssueTypePermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectOwner() -class RolesPermission(BasePermission): - get_permission = "view_role" - post_permission = "add_role" - put_permission = "change_role" - patch_permission = "change_role" - delete_permission = "delete_role" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class RolesPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectOwner() + update_perms = IsProjectOwner() + destroy_perms = IsProjectOwner() + list_perms = AllowAny() # Project Templates -class ProjectTemplatePermission(BasePermission): - # TODO: should be improved in permissions refactor - pass +class ProjectTemplatePermission(ResourcePermission): + retrieve_perms = AllowAny() + create_perms = IsSuperUser() + update_perms = IsSuperUser() + destroy_perms = IsSuperUser() + list_perms = AllowAny() diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index 54852219..79564d06 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -18,15 +18,17 @@ from django.db.models.loading import get_model from django.shortcuts import get_object_or_404 from rest_framework.response import Response -from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated from taiga.base import exceptions as exc +from taiga.base.api import viewsets from .serializers import ResolverSerializer +from taiga.permissions.service import user_has_perm + +from . import permissions class ResolverViewSet(viewsets.ViewSet): - permission_classes = (IsAuthenticated,) + permission_classes = (permissions.ResolverPermission,) def list(self, request, **kwargs): serializer = ResolverSerializer(data=request.QUERY_PARAMS) @@ -38,17 +40,19 @@ class ResolverViewSet(viewsets.ViewSet): project_model = get_model("projects", "Project") project = get_object_or_404(project_model, slug=data["project"]) + self.check_permissions(request, "list", project) + result = { "project": project.pk } - if data["us"]: + if data["us"] and user_has_perm(request.user, "view_us", project): result["us"] = get_object_or_404(project.user_stories.all(), ref=data["us"]).pk - if data["task"]: + if data["task"] and user_has_perm(request.user, "view_tasks", project): result["task"] = get_object_or_404(project.tasks.all(), ref=data["task"]).pk - if data["issue"]: + if data["issue"] and user_has_perm(request.user, "view_issues", project): result["issue"] = get_object_or_404(project.issues.all(), ref=data["issue"]).pk - if data["milestone"]: + if data["milestone"] and user_has_perm(request.user, "view_milestones", project): result["milestone"] = get_object_or_404(project.milestones.all(), slug=data["milestone"]).pk return Response(result) diff --git a/taiga/projects/references/permissions.py b/taiga/projects/references/permissions.py new file mode 100644 index 00000000..01266e31 --- /dev/null +++ b/taiga/projects/references/permissions.py @@ -0,0 +1,22 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, AllowAny) + + +class ResolverPermission(ResourcePermission): + list_perms = HasProjectPerm('view_project') diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index c24376ca..e20e4aaa 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -18,10 +18,10 @@ from os import path from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from taiga.base.serializers import PickleField, JsonField -from taiga.users.serializers import UserSerializer +from taiga.base.serializers import JsonField, PgArrayField from taiga.users.models import Role, User from taiga.users.services import get_photo_or_gravatar_url +from taiga.users.serializers import UserSerializer from . import models @@ -131,6 +131,9 @@ class ProjectMembershipSerializer(serializers.ModelSerializer): class ProjectSerializer(serializers.ModelSerializer): + tags = PgArrayField(required=False) + anon_permissions = PgArrayField(required=False) + public_permissions = PgArrayField(required=False) stars = serializers.SerializerMethodField("get_stars_number") class Meta: @@ -143,10 +146,16 @@ class ProjectSerializer(serializers.ModelSerializer): return getattr(obj, "stars_count", 0) def validate_slug(self, attrs, source): - project_with_slug = models.Project.objects.filter(slug=attrs[source]) + if self.object: + project_with_slug = models.Project.objects.filter(slug=attrs[source]).exclude(pk=self.object.pk) + else: + project_with_slug = models.Project.objects.filter(slug=attrs[source]) + if source == "slug" and project_with_slug.exists(): raise serializers.ValidationError(_("Slug duplicated for the project")) + return attrs + class ProjectDetailSerializer(ProjectSerializer): roles = serializers.SerializerMethodField("get_list_of_roles") @@ -187,6 +196,8 @@ class ProjectRoleSerializer(serializers.ModelSerializer): class RoleSerializer(serializers.ModelSerializer): + permissions = PgArrayField(required=False) + class Meta: model = Role fields = ('id', 'name', 'permissions', 'computable', 'project', 'order') diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py index 8a0beda1..47c277a4 100644 --- a/taiga/projects/services/bulk_update_order.py +++ b/taiga/projects/services/bulk_update_order.py @@ -31,6 +31,7 @@ def bulk_update_userstory_status_order(project, user, data): for id, order in data: cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") cursor.close() @@ -48,6 +49,7 @@ def bulk_update_points_order(project, user, data): for id, order in data: cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") cursor.close() @@ -65,6 +67,7 @@ def bulk_update_task_status_order(project, user, data): for id, order in data: cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") cursor.close() @@ -82,6 +85,7 @@ def bulk_update_issue_status_order(project, user, data): for id, order in data: cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") cursor.close() @@ -99,6 +103,7 @@ def bulk_update_issue_type_order(project, user, data): for id, order in data: cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") cursor.close() @@ -116,6 +121,7 @@ def bulk_update_priority_order(project, user, data): for id, order in data: cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") cursor.close() @@ -133,4 +139,5 @@ def bulk_update_severity_order(project, user, data): for id, order in data: cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") cursor.close() diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py index a073095d..e06a84e4 100644 --- a/taiga/projects/services/stats.py +++ b/taiga/projects/services/stats.py @@ -27,12 +27,19 @@ def _get_milestones_stats_for_backlog(project): current_evolution = 0 current_team_increment = 0 current_client_increment = 0 - optimal_points_per_sprint = project.total_story_points / (project.total_milestones) + + optimal_points_per_sprint = 0 + if project.total_story_points and project.total_milestones: + optimal_points_per_sprint = project.total_story_points / project.total_milestones + future_team_increment = sum(project.future_team_increment.values()) future_client_increment = sum(project.future_client_increment.values()) milestones = project.milestones.order_by('estimated_start') + optimal_points = 0 + team_increment = 0 + client_increment = 0 for current_milestone in range(0, max(milestones.count(), project.total_milestones)): optimal_points = (project.total_story_points - (optimal_points_per_sprint * current_milestone)) @@ -65,7 +72,7 @@ def _get_milestones_stats_for_backlog(project): optimal_points -= optimal_points_per_sprint evolution = (project.total_story_points - current_evolution - if current_evolution is not None else None) + if current_evolution is not None and project.total_story_points else None) yield { 'name': 'Project End', 'optimal': optimal_points, diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 027a981d..56ffa2b6 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -44,8 +44,8 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, model = models.Task serializer_class = serializers.TaskNeighborsSerializer list_serializer_class = serializers.TaskSerializer - permission_classes = (IsAuthenticated, permissions.TaskPermission) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.TaskPermission,) + filter_backends = (filters.CanViewTasksFilterBackend,) filter_fields = ["user_story", "milestone", "project"] def pre_save(self, obj): @@ -58,10 +58,6 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, def pre_conditions_on_save(self, obj): super().pre_conditions_on_save(obj) - if (obj.project.owner != self.request.user and - obj.project.memberships.filter(user=self.request.user).count() == 0): - raise exc.PermissionDenied(_("You don't have permissions for add/modify this task.")) - if obj.milestone and obj.milestone.project != obj.project: raise exc.PermissionDenied(_("You don't have permissions for add/modify this task.")) @@ -88,8 +84,7 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, project = get_object_or_404(Project, id=project_id) user_story = get_object_or_404(UserStory, id=us_id) - if request.user != project.owner and not has_project_perm(request.user, project, 'add_task'): - raise exc.PermissionDenied(_("You don't have permisions to create tasks.")) + self.check_permissions(request, 'bulk_create', project) tasks = services.create_tasks_in_bulk(bulk_tasks, callback=self.post_save, project=project, user_story=user_story, owner=request.user, diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 2e8a90bc..e22b3065 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -14,14 +14,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.permissions import BasePermission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, AllowAny) -class TaskPermission(BasePermission): - get_permission = "view_task" - post_permission = "add_task" - put_permission = "change_task" - patch_permission = "change_task" - delete_permission = "delete_task" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class TaskPermission(ResourcePermission): + enought_perms = IsProjectOwner() + global_perms = None + retrieve_perms = HasProjectPerm('view_tasks') + create_perms = HasProjectPerm('add_task') + update_perms = HasProjectPerm('modify_task') + destroy_perms = HasProjectPerm('delete_task') + list_perms = AllowAny() + bulk_create_perms = HasProjectPerm('add_task') diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 20c7a90d..667178df 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -46,9 +46,9 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi model = models.UserStory serializer_class = serializers.UserStoryNeighborsSerializer list_serializer_class = serializers.UserStorySerializer - permission_classes = (IsAuthenticated, permissions.UserStoryPermission) + permission_classes = (permissions.UserStoryPermission,) - filter_backends = (filters.IsProjectMemberFilterBackend, filters.TagsFilter) + filter_backends = (filters.CanViewUsFilterBackend, filters.TagsFilter) retrieve_exclude_filters = (filters.TagsFilter,) filter_fields = ['project', 'milestone', 'milestone__isnull', 'status', 'is_archived'] @@ -76,8 +76,7 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi project = get_object_or_404(Project, id=project_id) - if request.user != project.owner and not has_project_perm(request.user, project, 'add_userstory'): - raise exc.PermissionDenied(_("You don't have permisions to create user stories.")) + self.check_permissions(request, 'bulk_create', project) user_stories = services.create_userstories_in_bulk( bulk_stories, callback=self.post_save, project=project, owner=request.user) @@ -103,8 +102,7 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi project = get_object_or_404(Project, id=project_id) - if request.user != project.owner and not has_project_perm(request.user, project, 'change_userstory'): - raise exc.PermissionDenied(_("You don't have permisions to create user stories.")) + self.check_permissions(request, 'bulk_update_order', project) services.update_userstories_order_in_bulk(bulk_stories) @@ -134,16 +132,3 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi obj.owner = self.request.user super().pre_save(obj) - - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) - - if (obj.project.owner != self.request.user and - obj.project.memberships.filter(user=self.request.user).count() == 0): - raise exc.PermissionDenied(_("You don't have permissions for add/modify this user story")) - - if obj.milestone and obj.milestone.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions for add/modify this user story")) - - if obj.status and obj.status.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions for add/modify this user story")) diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index fc20e6fd..5a07d88d 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -14,14 +14,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.permissions import BasePermission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsAuthenticated, IsProjectOwner, + AllowAny, IsSuperUser) -class UserStoryPermission(BasePermission): - get_permission = "view_userstory" - post_permission = "add_userstory" - put_permission = "change_userstory" - patch_permission = "change_userstory" - delete_permission = "delete_userstory" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class UserStoryPermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_us') + create_perms = HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us') + update_perms = HasProjectPerm('modify_us') + destroy_perms = HasProjectPerm('delete_us') + list_perms = AllowAny() + bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) + bulk_update_order_perms = HasProjectPerm('modify_us') diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 8418c6f6..c3175942 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -55,7 +55,6 @@ class UserStorySerializer(serializers.ModelSerializer): points_modelcls = get_model("projects", "Points") - obj.project.update_role_points() if role_points: for role_id, points_id in role_points.items(): role_points = obj.role_points.get(role__id=role_id) diff --git a/taiga/projects/votes/models.py b/taiga/projects/votes/models.py index 7742978f..5457c3ac 100644 --- a/taiga/projects/votes/models.py +++ b/taiga/projects/votes/models.py @@ -32,6 +32,12 @@ class Votes(models.Model): verbose_name_plural = _("Votes") unique_together = ("content_type", "object_id") + @property + def project(self): + if hasattr(self.content_object, 'project'): + return self.content_object.project + return None + def __str__(self): return self.count @@ -48,5 +54,11 @@ class Vote(models.Model): verbose_name_plural = _("Votes") unique_together = ("content_type", "object_id", "user") + @property + def project(self): + if hasattr(self.content_object, 'project'): + return self.content_object.project + return None + def __str__(self): return self.user diff --git a/taiga/projects/votes/services.py b/taiga/projects/votes/services.py index d8884001..e09c96b3 100644 --- a/taiga/projects/votes/services.py +++ b/taiga/projects/votes/services.py @@ -34,7 +34,7 @@ def add_vote(obj, user): """ obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj) with atomic(): - _, created = Vote.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user) + vote, created = Vote.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user) if not created: return @@ -42,6 +42,7 @@ def add_vote(obj, user): votes, _ = Votes.objects.get_or_create(content_type=obj_type, object_id=obj.id) votes.count = F('count') + 1 votes.save() + return vote def remove_vote(obj, user): @@ -74,7 +75,6 @@ def get_voters(obj): :return: User queryset object representing the users that voted the object. """ obj_type = get_model("contenttypes", "ContentType").objects.get_for_model(obj) - return get_user_model().objects.filter(votes__content_type=obj_type, votes__object_id=obj.id) diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index 2cb865a1..94b5d689 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -41,8 +41,8 @@ from . import serializers class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): model = models.WikiPage serializer_class = serializers.WikiPageSerializer - permission_classes = (IsAuthenticated,) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.WikiPagePermission,) + filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ("project", "slug") @list_route(methods=["POST"]) @@ -57,16 +57,12 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, raise exc.WrongArguments({"project_id": "No project_id parameter"}) project = get_object_or_404(Project, pk=project_id) + + self.check_permissions(request, "render", project) + data = mdrender(project, content) return Response({"data": data}) - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) - - if (obj.project.owner != self.request.user and - obj.project.memberships.filter(user=self.request.user).count() == 0): - raise exc.PermissionDenied(_("You don't haver permissions for add/modify " - "this wiki page.")) def pre_save(self, obj): if not obj.owner: obj.owner = self.request.user @@ -77,6 +73,6 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, class WikiLinkViewSet(ModelCrudViewSet): model = models.WikiLink serializer_class = serializers.WikiLinkSerializer - permission_classes = (IsAuthenticated,) - filter_backends = (filters.IsProjectMemberFilterBackend,) + permission_classes = (permissions.WikiLinkPermission,) + filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ["project"] diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py index d8dd82c9..5bd0d2a9 100644 --- a/taiga/projects/wiki/permissions.py +++ b/taiga/projects/wiki/permissions.py @@ -14,14 +14,25 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.permissions import BasePermission +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, AllowAny) -class WikiPagePermission(BasePermission): - get_permission = "view_wikipage" - post_permission = "add_wikipage" - put_permission = "change_wikipage" - patch_permission = "change_wikipage" - delete_permission = "delete_wikipage" - safe_methods = ["HEAD", "OPTIONS"] - path_to_project = ["project"] +class WikiPagePermission(ResourcePermission): + enought_perms = IsProjectOwner() + global_perms = None + retrieve_perms = HasProjectPerm('view_wiki_pages') + create_perms = HasProjectPerm('add_wiki_page') + update_perms = HasProjectPerm('modify_wiki_page') + destroy_perms = HasProjectPerm('delete_wiki_page') + list_perms = AllowAny() + render_perms = AllowAny() + +class WikiLinkPermission(ResourcePermission): + enought_perms = IsProjectOwner() + global_perms = None + retrieve_perms = HasProjectPerm('view_wiki_links') + create_perms = HasProjectPerm('add_wiki_link') + update_perms = HasProjectPerm('modify_wiki_link') + destroy_perms = HasProjectPerm('delete_wiki_link') + list_perms = AllowAny() diff --git a/taiga/routers.py b/taiga/routers.py index 39b61f99..f33022e3 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -44,10 +44,6 @@ from taiga.searches.api import SearchViewSet router.register(r"search", SearchViewSet, base_name="search") -from taiga.projects.api import ProjectAdminViewSet -router.register(r"site-projects", ProjectAdminViewSet, base_name="site-projects") - - # Projects & Types from taiga.projects.api import RolesViewSet from taiga.projects.api import ProjectViewSet @@ -61,13 +57,9 @@ from taiga.projects.api import IssueTypeViewSet from taiga.projects.api import PriorityViewSet from taiga.projects.api import SeverityViewSet from taiga.projects.api import ProjectTemplateViewSet -from taiga.projects.api import FansViewSet -from taiga.projects.api import StarredViewSet router.register(r"roles", RolesViewSet, base_name="roles") router.register(r"projects", ProjectViewSet, base_name="projects") -router.register(r"projects/(?P\d+)/fans", FansViewSet, base_name="project-fans") -router.register(r"users/(?P\d+)/starred", StarredViewSet, base_name="user-starred") router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates") router.register(r"memberships", MembershipViewSet, base_name="memberships") router.register(r"invitations", InvitationViewSet, base_name="invitations") diff --git a/taiga/searches/api.py b/taiga/searches/api.py index 818ef625..7d5f36ea 100644 --- a/taiga/searches/api.py +++ b/taiga/searches/api.py @@ -24,6 +24,7 @@ from taiga.projects.userstories.serializers import UserStorySerializer from taiga.projects.tasks.serializers import TaskSerializer from taiga.projects.issues.serializers import IssueSerializer from taiga.projects.wiki.serializers import WikiPageSerializer +from taiga.permissions.service import user_has_perm from . import services @@ -40,22 +41,22 @@ class SearchViewSet(viewsets.ViewSet): except (project_model.DoesNotExist, TypeError): raise excp.PermissionDenied({"detail": "Wrong project id"}) - result = { - "userstories": self._search_user_stories(project, text), - "tasks": self._search_tasks(project, text), - "issues": self._search_issues(project, text), - "wikipages": self._search_wiki_pages(project, text) - } + result = {} + if user_has_perm(request.user, "view_us", project): + result["userstories"] = self._search_user_stories(project, text) + if user_has_perm(request.user, "view_tasks", project): + result["tasks"] = self._search_tasks(project, text) + if user_has_perm(request.user, "view_issues", project): + result["issues"] = self._search_issues(project, text) + if user_has_perm(request.user, "view_wiki_pages", project): + result["wikipages"] = self._search_wiki_pages(project, text) result["count"] = sum(map(lambda x: len(x), result.values())) return Response(result) def _get_project(self, project_id): project_model = get_model("projects", "Project") - own_projects = (project_model.objects - .filter(members=self.request.user)) - - return own_projects.get(pk=project_id) + return project_model.objects.get(pk=project_id) def _search_user_stories(self, project, text): queryset = services.search_user_stories(project, text) diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index 96f5f7b0..2db4c7f6 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -23,8 +23,8 @@ from taiga.base.api import GenericViewSet from . import serializers from . import service - -# TODO: Set Timelines permissions +from . import permissions +from . import models class TimelineViewSet(GenericViewSet): @@ -36,13 +36,13 @@ class TimelineViewSet(GenericViewSet): app_name, model = self.content_type.split(".", 1) return get_object_or_404(ContentType, app_label=app_name, model=model) - def get_object(self): + def get_queryset(self): ct = self.get_content_type() model_cls = ct.model_class() qs = model_cls.objects.all() filtered_qs = self.filter_queryset(qs) - return super().get_object(queryset=filtered_qs) + return filtered_qs def response_for_queryset(self, queryset): # Switch between paginated or standard style responses @@ -61,13 +61,17 @@ class TimelineViewSet(GenericViewSet): def retrieve(self, request, pk): obj = self.get_object() + self.check_permissions(request, "retrieve", obj) + qs = service.get_timeline(obj) return self.response_for_queryset(qs) class UserTimeline(TimelineViewSet): content_type = "users.user" + permission_classes = (permissions.UserTimelinePermission,) class ProjectTimeline(TimelineViewSet): content_type = "projects.project" + permission_classes = (permissions.ProjectTimelinePermission,) diff --git a/taiga/timeline/permissions.py b/taiga/timeline/permissions.py new file mode 100644 index 00000000..4823a6e5 --- /dev/null +++ b/taiga/timeline/permissions.py @@ -0,0 +1,26 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api.permissions import (ResourcePermission, HasProjectPerm, + IsProjectOwner, AllowAny) + + +class UserTimelinePermission(ResourcePermission): + retrieve_perms = AllowAny() + + +class ProjectTimelinePermission(ResourcePermission): + retrieve_perms = HasProjectPerm('view_project') diff --git a/taiga/urls.py b/taiga/urls.py index 74daefd7..a7f4d876 100644 --- a/taiga/urls.py +++ b/taiga/urls.py @@ -20,7 +20,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin from .routers import router -from .projects.attachments.views import RawAttachmentView +from .projects.attachments.api import RawAttachmentView diff --git a/taiga/users/admin.py b/taiga/users/admin.py index 92d0e0ce..c364929c 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -41,7 +41,7 @@ class RoleAdmin(admin.ModelAdmin): db_field, request=request, **kwargs) -admin.site.register(Role, RoleAdmin) +# admin.site.register(Role, RoleAdmin) class UserAdmin(DjangoUserAdmin): diff --git a/taiga/users/api.py b/taiga/users/api.py index 250f695a..46808f2c 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -25,16 +25,21 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.response import Response from rest_framework.filters import BaseFilterBackend from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework import status, viewsets +from rest_framework import status from djmail.template_mail import MagicMailBuilder -from taiga.base.decorators import list_route, action +from taiga.base.decorators import list_route, detail_route +from taiga.base.decorators import action from taiga.base import exceptions as exc -from taiga.base.api import ModelCrudViewSet, RetrieveModelMixin, ModelListViewSet +from taiga.base.api import ModelCrudViewSet +from taiga.base.api import ModelListViewSet +from taiga.projects.votes import services as votes_service +from taiga.projects.serializers import StarredSerializer -from .models import User, Role -from .serializers import UserSerializer, RecoverySerializer +from . import models +from . import serializers +from . import permissions class MembersFilterBackend(BaseFilterBackend): @@ -55,31 +60,45 @@ class MembersFilterBackend(BaseFilterBackend): class UsersViewSet(ModelCrudViewSet): - permission_classes = (IsAuthenticated,) - serializer_class = UserSerializer - queryset = User.objects.all() - filter_backends = (MembersFilterBackend,) + permission_classes = (permissions.UserPermission,) + serializer_class = serializers.UserSerializer + queryset = models.User.objects.all() def pre_conditions_on_save(self, obj): - if not self.request.user.is_superuser and obj.id != self.request.user.id: - raise exc.PreconditionError() + if self.request.user.is_superuser: + return + + if obj.id == self.request.user.id: + return + + if obj.id is None: + return + + raise exc.PreconditionError() def pre_conditions_on_delete(self, obj): - if not self.request.user.is_superuser and obj.id != self.request.user.id: - raise exc.PreconditionError() + if self.request.user.is_superuser: + return - @list_route(permission_classes=[AllowAny], methods=["POST"]) + if obj.id == self.request.user.id: + return + + raise exc.PreconditionError() + + @list_route(methods=["POST"]) def password_recovery(self, request, pk=None): username_or_email = request.DATA.get('username', None) + self.check_permissions(request, "password_recovery", None) + if not username_or_email: raise exc.WrongArguments(_("Invalid username or email")) try: - queryset = User.objects.all() + queryset = models.User.objects.all() user = queryset.get(Q(username=username_or_email) | Q(email=username_or_email)) - except User.DoesNotExist: + except models.User.DoesNotExist: raise exc.WrongArguments(_("Invalid username or email")) user.token = str(uuid.uuid1()) @@ -92,27 +111,36 @@ class UsersViewSet(ModelCrudViewSet): return Response({"detail": _("Mail sended successful!"), "email": user.email}) - @list_route(permission_classes=[AllowAny], methods=["POST"]) + @list_route(methods=["POST"]) def change_password_from_recovery(self, request, pk=None): """ Change password with token (from password recovery step). """ - serializer = RecoverySerializer(data=request.DATA, many=False) + + self.check_permissions(request, "change_password_from_recovery", None) + + serializer = serializers.RecoverySerializer(data=request.DATA, many=False) if not serializer.is_valid(): raise exc.WrongArguments(_("Token is invalid")) - user = User.objects.get(token=serializer.data["token"]) + try: + user = models.User.objects.get(token=serializer.data["token"]) + except models.User.DoesNotExist: + raise exc.WrongArguments(_("Token is invalid")) + user.set_password(serializer.data["password"]) user.token = None user.save(update_fields=["password", "token"]) return Response(status=status.HTTP_204_NO_CONTENT) - @list_route(permission_classes=[IsAuthenticated], methods=["POST"]) + @list_route(methods=["POST"]) def change_password(self, request, pk=None): """ Change password to current logged user. """ + self.check_permissions(request, "change_password", None) + password = request.DATA.get("password") if not password: @@ -124,3 +152,12 @@ class UsersViewSet(ModelCrudViewSet): request.user.set_password(password) request.user.save(update_fields=["password"]) return Response(status=status.HTTP_204_NO_CONTENT) + + @detail_route(methods=["GET"]) + def starred(self, request, pk=None): + user = self.get_object() + self.check_permissions(request, 'starred', user) + + stars = votes_service.get_voted(user.pk, model=get_model('projects', 'Project')) + stars_data = StarredSerializer(stars, many=True) + return Response(stars_data.data) diff --git a/taiga/users/models.py b/taiga/users/models.py index a7b1c8a1..593e2c25 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -21,7 +21,10 @@ from django.contrib.auth.models import UserManager, AbstractBaseUser from django.core import validators from django.utils import timezone +from djorm_pgarray.fields import TextArrayField + from taiga.base.utils.slug import slugify_uniquely +from taiga.permissions.permissions import MEMBERS_PERMISSIONS import random import re @@ -124,8 +127,10 @@ class Role(models.Model): verbose_name=_("name")) slug = models.SlugField(max_length=250, null=False, blank=True, verbose_name=_("slug")) - permissions = models.ManyToManyField("auth.Permission", related_name="roles", - verbose_name=_("permissions")) + permissions = TextArrayField(blank=True, null=True, + default=[], + verbose_name=_("permissions"), + choices=MEMBERS_PERMISSIONS) order = models.IntegerField(default=10, null=False, blank=False, verbose_name=_("order")) project = models.ForeignKey("projects.Project", null=False, blank=False, diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py new file mode 100644 index 00000000..f3c2d921 --- /dev/null +++ b/taiga/users/permissions.py @@ -0,0 +1,38 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api.permissions import (ResourcePermission, IsSuperUser, + AllowAny, PermissionComponent, + IsAuthenticated) + + +class IsTheSameUser(PermissionComponent): + def check_permissions(self, request, view, obj=None): + return request.user.is_authenticated() and request.user.pk == obj.pk + + +class UserPermission(ResourcePermission): + enought_perms = IsSuperUser() + global_perms = None + retrieve_perms = AllowAny() + create_perms = AllowAny() + update_perms = IsTheSameUser() + destroy_perms = IsTheSameUser() + list_perms = AllowAny() + password_recovery_perms = AllowAny() + change_password_from_recovery_perms = AllowAny() + change_password_perms = IsAuthenticated() + starred_perms = AllowAny() diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 2286de4a..1128d274 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -42,12 +42,3 @@ class UserSerializer(serializers.ModelSerializer): class RecoverySerializer(serializers.Serializer): token = serializers.CharField(max_length=200) password = serializers.CharField(min_length=6) - - def validate_token(self, attrs, source): - token = attrs[source] - try: - user = User.objects.get(token=token) - except User.DoesNotExist: - raise serializers.ValidationError(_("invalid token")) - - return attrs diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py index a7917f31..1ed184a9 100644 --- a/taiga/userstorage/api.py +++ b/taiga/userstorage/api.py @@ -32,14 +32,17 @@ class StorageEntriesViewSet(ModelCrudViewSet): model = models.StorageEntry filter_backends = (filters.StorageEntriesFilterBackend,) serializer_class = serializers.StorageEntrySerializer - permission_classes = (IsAuthenticated, permissions.StorageEntriesPermission) + permission_classes = [permissions.StorageEntriesPermission] lookup_field = "key" def get_queryset(self): + if self.request.user.is_anonymous(): + return self.model.objects.none() return self.request.user.storage_entries.all() def pre_save(self, obj): - obj.owner = self.request.user + if self.request.user.is_authenticated(): + obj.owner = self.request.user def create(self, *args, **kwargs): try: diff --git a/taiga/userstorage/permissions.py b/taiga/userstorage/permissions.py index 933987e8..af9bd6eb 100644 --- a/taiga/userstorage/permissions.py +++ b/taiga/userstorage/permissions.py @@ -14,9 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.permissions import Permission +from taiga.base.api.permissions import ResourcePermission, IsAuthenticated, DenyAll -class StorageEntriesPermission(Permission): - def has_object_permission(self, request, view, obj): - return request.user == obj.owner +class StorageEntriesPermission(ResourcePermission): + enought_perms = IsAuthenticated() + global_perms = DenyAll() diff --git a/tests/factories.py b/tests/factories.py index 909aaf53..ae980c9d 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -44,6 +44,7 @@ class ProjectTemplateFactory(Factory): name = "Template name" slug = settings.DEFAULT_PROJECT_TEMPLATE + description = factory.Sequence(lambda n: "Description {}".format(n)) us_statuses = [] points = [] @@ -80,6 +81,28 @@ class PointsFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") +class AttachmentFactory(Factory): + FACTORY_FOR = get_model("attachments", "Attachment") + project = factory.SubFactory("tests.factories.ProjectFactory") + owner = factory.SubFactory("tests.factories.UserFactory") + + +class UserStoryAttachmentFactory(AttachmentFactory): + content_object = factory.SubFactory("tests.factories.UserStoryFactory") + + +class TaskAttachmentFactory(AttachmentFactory): + content_object = factory.SubFactory("tests.factories.TaskFactory") + + +class IssueAttachmentFactory(AttachmentFactory): + content_object = factory.SubFactory("tests.factories.IssueFactory") + + +class WikiAttachmentFactory(AttachmentFactory): + content_object = factory.SubFactory("tests.factories.WikiFactory") + + class RolePointsFactory(Factory): FACTORY_FOR = get_model("userstories", "RolePoints") @@ -131,17 +154,6 @@ class UserStoryStatusFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") -class TaskFactory(Factory): - FACTORY_FOR = get_model("tasks", "Task") - - ref = factory.Sequence(lambda n: n) - owner = factory.SubFactory("tests.factories.UserFactory") - subject = factory.Sequence(lambda n: "Task {}".format(n)) - user_story = factory.SubFactory("tests.factories.UserStoryFactory") - status = factory.SubFactory("tests.factories.TaskStatusFactory") - project = factory.SubFactory("tests.factories.ProjectFactory") - milestone = factory.SubFactory("tests.factories.MilestoneFactory") - class TaskStatusFactory(Factory): FACTORY_FOR = get_model("projects", "TaskStatus") @@ -177,12 +189,14 @@ class IssueFactory(Factory): class TaskFactory(Factory): FACTORY_FOR = get_model("tasks", "Task") + ref = factory.Sequence(lambda n: n) subject = factory.Sequence(lambda n: "Task {}".format(n)) description = factory.Sequence(lambda n: "Task {} description".format(n)) owner = factory.SubFactory("tests.factories.UserFactory") project = factory.SubFactory("tests.factories.ProjectFactory") status = factory.SubFactory("tests.factories.TaskStatusFactory") milestone = factory.SubFactory("tests.factories.MilestoneFactory") + user_story = factory.SubFactory("tests.factories.UserStoryFactory") class WikiPageFactory(Factory): @@ -194,6 +208,15 @@ class WikiPageFactory(Factory): content = factory.Sequence(lambda n: "Wiki Page {} content".format(n)) +class WikiLinkFactory(Factory): + FACTORY_FOR = get_model("wiki", "WikiLink") + + project = factory.SubFactory("tests.factories.ProjectFactory") + title = factory.Sequence(lambda n: "Wiki Link {} title".format(n)) + href = factory.Sequence(lambda n: "link-{}".format(n)) + order = factory.Sequence(lambda n: n) + + class IssueStatusFactory(Factory): FACTORY_FOR = get_model("projects", "IssueStatus") @@ -201,10 +224,10 @@ class IssueStatusFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") -class TaskStatusFactory(Factory): - FACTORY_FOR = get_model("projects", "TaskStatus") +class UserStoryStatusFactory(Factory): + FACTORY_FOR = get_model("projects", "UserStoryStatus") - name = factory.Sequence(lambda n: "Issue Status {}".format(n)) + name = factory.Sequence(lambda n: "User Story Status {}".format(n)) project = factory.SubFactory("tests.factories.ProjectFactory") diff --git a/tests/integration/resources_permissions/__init__.py b/tests/integration/resources_permissions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py new file mode 100644 index 00000000..86b80a51 --- /dev/null +++ b/tests/integration/resources_permissions/test_attachment_resources.py @@ -0,0 +1,594 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects.attachments.serializers import AttachmentSerializer + +from tests import factories as f +from tests.utils import helper_test_http_method, helper_test_http_method_and_count, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_user_story = f.UserStoryFactory(project=m.public_project, ref=1) + m.public_task = f.TaskFactory(project=m.public_project, ref=2) + m.public_issue = f.IssueFactory(project=m.public_project, ref=3) + m.public_wiki = f.WikiPageFactory(project=m.public_project, slug=4) + + m.public_user_story_attachment = f.UserStoryAttachmentFactory(project=m.public_project, content_object=m.public_user_story) + m.public_task_attachment = f.TaskAttachmentFactory(project=m.public_project, content_object=m.public_task) + m.public_issue_attachment = f.IssueAttachmentFactory(project=m.public_project, content_object=m.public_issue) + m.public_wiki_attachment = f.WikiAttachmentFactory(project=m.public_project, content_object=m.public_wiki) + + m.private_user_story1 = f.UserStoryFactory(project=m.private_project1, ref=5) + m.private_task1 = f.TaskFactory(project=m.private_project1, ref=6) + m.private_issue1 = f.IssueFactory(project=m.private_project1, ref=7) + m.private_wiki1 = f.WikiPageFactory(project=m.private_project1, slug=8) + + m.private_user_story1_attachment = f.UserStoryAttachmentFactory(project=m.private_project1, content_object=m.private_user_story1) + m.private_task1_attachment = f.TaskAttachmentFactory(project=m.private_project1, content_object=m.private_task1) + m.private_issue1_attachment = f.IssueAttachmentFactory(project=m.private_project1, content_object=m.private_issue1) + m.private_wiki1_attachment = f.WikiAttachmentFactory(project=m.private_project1, content_object=m.private_wiki1) + + m.private_user_story2 = f.UserStoryFactory(project=m.private_project2, ref=9) + m.private_task2 = f.TaskFactory(project=m.private_project2, ref=10) + m.private_issue2 = f.IssueFactory(project=m.private_project2, ref=11) + m.private_wiki2 = f.WikiPageFactory(project=m.private_project2, slug=12) + + m.private_user_story2_attachment = f.UserStoryAttachmentFactory(project=m.private_project2, content_object=m.private_user_story2) + m.private_task2_attachment = f.TaskAttachmentFactory(project=m.private_project2, content_object=m.private_task2) + m.private_issue2_attachment = f.IssueAttachmentFactory(project=m.private_project2, content_object=m.private_issue2) + m.private_wiki2_attachment = f.WikiAttachmentFactory(project=m.private_project2, content_object=m.private_wiki2) + + return m + + +def test_user_story_attachment_retrieve(client, data): + public_url = reverse('userstory-attachments-detail', kwargs={"pk": data.public_user_story_attachment.pk}) + private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story1_attachment.pk}) + private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_attachment_retrieve(client, data): + public_url = reverse('task-attachments-detail', kwargs={"pk": data.public_task_attachment.pk}) + private_url1 = reverse('task-attachments-detail', kwargs={"pk": data.private_task1_attachment.pk}) + private_url2 = reverse('task-attachments-detail', kwargs={"pk": data.private_task2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_attachment_retrieve(client, data): + public_url = reverse('issue-attachments-detail', kwargs={"pk": data.public_issue_attachment.pk}) + private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue1_attachment.pk}) + private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_attachment_retrieve(client, data): + public_url = reverse('wiki-attachments-detail', kwargs={"pk": data.public_wiki_attachment.pk}) + private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki1_attachment.pk}) + private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_attachment_update(client, data): + public_url = reverse('userstory-attachments-detail', kwargs={"pk": data.public_user_story_attachment.pk}) + private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story1_attachment.pk}) + private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_user_story_attachment).data + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'put', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_attachment_update(client, data): + public_url = reverse('task-attachments-detail', kwargs={"pk": data.public_task_attachment.pk}) + private_url1 = reverse('task-attachments-detail', kwargs={"pk": data.private_task1_attachment.pk}) + private_url2 = reverse('task-attachments-detail', kwargs={"pk": data.private_task2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_task_attachment).data + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'put', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_attachment_update(client, data): + public_url = reverse('issue-attachments-detail', kwargs={"pk": data.public_issue_attachment.pk}) + private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue1_attachment.pk}) + private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_issue_attachment).data + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'put', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_attachment_update(client, data): + public_url = reverse('wiki-attachments-detail', kwargs={"pk": data.public_wiki_attachment.pk}) + private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki1_attachment.pk}) + private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_wiki_attachment).data + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'put', public_url, attachment_data, users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_attachment_patch(client, data): + public_url = reverse('userstory-attachments-detail', kwargs={"pk": data.public_user_story_attachment.pk}) + private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story1_attachment.pk}) + private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test"} + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_attachment_patch(client, data): + public_url = reverse('task-attachments-detail', kwargs={"pk": data.public_task_attachment.pk}) + private_url1 = reverse('task-attachments-detail', kwargs={"pk": data.private_task1_attachment.pk}) + private_url2 = reverse('task-attachments-detail', kwargs={"pk": data.private_task2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test"} + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_attachment_patch(client, data): + public_url = reverse('issue-attachments-detail', kwargs={"pk": data.public_issue_attachment.pk}) + private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue1_attachment.pk}) + private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test"} + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_attachment_patch(client, data): + public_url = reverse('wiki-attachments-detail', kwargs={"pk": data.public_wiki_attachment.pk}) + private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki1_attachment.pk}) + private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test"} + attachment_data = JSONRenderer().render(attachment_data) + + results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_attachment_delete(client, data): + public_url = reverse('userstory-attachments-detail', kwargs={"pk": data.public_user_story_attachment.pk}) + private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story1_attachment.pk}) + private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data.private_user_story2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_task_attachment_delete(client, data): + public_url = reverse('task-attachments-detail', kwargs={"pk": data.public_task_attachment.pk}) + private_url1 = reverse('task-attachments-detail', kwargs={"pk": data.private_task1_attachment.pk}) + private_url2 = reverse('task-attachments-detail', kwargs={"pk": data.private_task2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_issue_attachment_delete(client, data): + public_url = reverse('issue-attachments-detail', kwargs={"pk": data.public_issue_attachment.pk}) + private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue1_attachment.pk}) + private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data.private_issue2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_wiki_attachment_delete(client, data): + public_url = reverse('wiki-attachments-detail', kwargs={"pk": data.public_wiki_attachment.pk}) + private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki1_attachment.pk}) + private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data.private_wiki2_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, [None, data.registered_user]) + assert results == [401, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, [None, data.registered_user]) + assert results == [401, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + +def test_user_story_attachment_create(client, data): + url = reverse('userstory-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_user_story_attachment).data + attachment_data["id"] = None + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + results = helper_test_http_method(client, 'post', url, attachment_data, users) + assert results == [401, 403, 403, 201, 201] + + +def test_task_attachment_create(client, data): + url = reverse('task-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_task_attachment).data + attachment_data["id"] = None + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + results = helper_test_http_method(client, 'post', url, attachment_data, users) + assert results == [401, 403, 403, 201, 201] + + +def test_issue_attachment_create(client, data): + url = reverse('issue-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_issue_attachment).data + attachment_data["id"] = None + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + results = helper_test_http_method(client, 'post', url, attachment_data, users) + assert results == [401, 403, 403, 201, 201] + + +def test_wiki_attachment_create(client, data): + url = reverse('wiki-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data.public_wiki_attachment).data + attachment_data["id"] = None + attachment_data["description"] = "test" + attachment_data = JSONRenderer().render(attachment_data) + results = helper_test_http_method(client, 'post', url, attachment_data, users) + assert results == [401, 201, 201, 201, 201] + + +def test_user_story_attachment_list(client, data): + url = reverse('userstory-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', url, None, users) + assert results == [(200, 2), (200, 2), (200, 3), (200, 3), (200, 3)] + + +def test_task_attachment_list(client, data): + url = reverse('task-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', url, None, users) + assert results == [(200, 2), (200, 2), (200, 3), (200, 3), (200, 3)] + + +def test_issue_attachment_list(client, data): + url = reverse('issue-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', url, None, users) + assert results == [(200, 2), (200, 2), (200, 3), (200, 3), (200, 3)] + + +def test_wiki_attachment_list(client, data): + url = reverse('wiki-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', url, None, users) + assert results == [(200, 2), (200, 2), (200, 3), (200, 3), (200, 3)] diff --git a/tests/integration/resources_permissions/test_auth_resources.py b/tests/integration/resources_permissions/test_auth_resources.py new file mode 100644 index 00000000..296fb964 --- /dev/null +++ b/tests/integration/resources_permissions/test_auth_resources.py @@ -0,0 +1,52 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +def test_auth_create(client): + url = reverse('auth-list') + + user = f.UserFactory.create() + + login_data = json.dumps({ + "type": "normal", + "username": user.username, + "password": user.username, + }) + + result = client.post(url, login_data, content_type="application/json") + assert result.status_code == 200 + + +def test_auth_action_register(client, settings): + settings.PUBLIC_REGISTER_ENABLED = True + url = reverse('auth-register') + + register_data = json.dumps({ + "type": "public", + "username": "test", + "password": "test", + "full_name": "test", + "email": "test@test.com", + }) + + result = client.post(url, register_data, content_type="application/json") + assert result.status_code == 201 diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py new file mode 100644 index 00000000..0313eabc --- /dev/null +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -0,0 +1,167 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_user_story = f.UserStoryFactory(project=m.public_project, ref=1) + m.public_task = f.TaskFactory(project=m.public_project, ref=2) + m.public_issue = f.IssueFactory(project=m.public_project, ref=3) + m.public_wiki = f.WikiPageFactory(project=m.public_project, slug=4) + + m.private_user_story1 = f.UserStoryFactory(project=m.private_project1, ref=5) + m.private_task1 = f.TaskFactory(project=m.private_project1, ref=6) + m.private_issue1 = f.IssueFactory(project=m.private_project1, ref=7) + m.private_wiki1 = f.WikiPageFactory(project=m.private_project1, slug=8) + + m.private_user_story2 = f.UserStoryFactory(project=m.private_project2, ref=9) + m.private_task2 = f.TaskFactory(project=m.private_project2, ref=10) + m.private_issue2 = f.IssueFactory(project=m.private_project2, ref=11) + m.private_wiki2 = f.WikiPageFactory(project=m.private_project2, slug=12) + + return m + + +def test_user_story_history_retrieve(client, data): + public_url = reverse('userstory-history-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstory-history-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstory-history-detail', kwargs={"pk": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_history_retrieve(client, data): + public_url = reverse('task-history-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('task-history-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('task-history-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_history_retrieve(client, data): + public_url = reverse('issue-history-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issue-history-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issue-history-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_history_retrieve(client, data): + public_url = reverse('wiki-history-detail', kwargs={"pk": data.public_wiki.pk}) + private_url1 = reverse('wiki-history-detail', kwargs={"pk": data.private_wiki1.pk}) + private_url2 = reverse('wiki-history-detail', kwargs={"pk": data.private_wiki2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py new file mode 100644 index 00000000..48106047 --- /dev/null +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -0,0 +1,359 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.projects.issues.serializers import IssueSerializer +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from taiga.projects.votes.services import add_vote + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_issue = f.IssueFactory(project=m.public_project, + status__project=m.public_project, + severity__project=m.public_project, + priority__project=m.public_project, + type__project=m.public_project, + milestone__project=m.public_project) + m.private_issue1 = f.IssueFactory(project=m.private_project1, + status__project=m.private_project1, + severity__project=m.private_project1, + priority__project=m.private_project1, + type__project=m.private_project1, + milestone__project=m.private_project1) + m.private_issue2 = f.IssueFactory(project=m.private_project2, + status__project=m.private_project2, + severity__project=m.private_project2, + priority__project=m.private_project2, + type__project=m.private_project2, + milestone__project=m.private_project2) + + return m + + +def test_issue_retrieve(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_update(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + issue_data = IssueSerializer(data.public_issue).data + issue_data["subject"] = "test" + issue_data = JSONRenderer().render(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue1).data + issue_data["subject"] = "test" + issue_data = JSONRenderer().render(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue2).data + issue_data["subject"] = "test" + issue_data = JSONRenderer().render(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_delete(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_issue_list(client, data): + url = reverse('issues-list') + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 3 + assert response.status_code == 200 + + +def test_issue_create(client, data): + url = reverse('issues-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "severity": data.public_project.severities.all()[0].pk, + "priority": data.public_project.priorities.all()[0].pk, + "status": data.public_project.issue_statuses.all()[0].pk, + "type": data.public_project.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "severity": data.private_project1.severities.all()[0].pk, + "priority": data.private_project1.priorities.all()[0].pk, + "status": data.private_project1.issue_statuses.all()[0].pk, + "type": data.private_project1.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "severity": data.private_project2.severities.all()[0].pk, + "priority": data.private_project2.priorities.all()[0].pk, + "status": data.private_project2.issue_statuses.all()[0].pk, + "type": data.private_project2.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + +def test_issue_patch(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"subject": "test", "version": data.public_issue.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_issue1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_issue2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_action_upvote(client, data): + public_url = reverse('issues-upvote', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-upvote', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-upvote', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_action_downvote(client, data): + public_url = reverse('issues-downvote', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-downvote', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-downvote', kwargs={"pk": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_voters_list(client, data): + public_url = reverse('issue-voters-list', kwargs={"issue_id": data.public_issue.pk}) + private_url1 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue1.pk}) + private_url2 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + +def test_issue_voters_retrieve(client, data): + add_vote(data.public_issue, data.project_owner) + public_url = reverse('issue-voters-detail', kwargs={"issue_id": data.public_issue.pk, "pk": data.project_owner.pk}) + add_vote(data.private_issue1, data.project_owner) + private_url1 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue1.pk, "pk": data.project_owner.pk}) + add_vote(data.private_issue2, data.project_owner) + private_url2 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue2.pk, "pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py new file mode 100644 index 00000000..6f5c1957 --- /dev/null +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -0,0 +1,263 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.projects.milestones.serializers import MilestoneSerializer +from taiga.projects.milestones.models import Milestone +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_milestone = f.MilestoneFactory(project=m.public_project) + m.private_milestone1 = f.MilestoneFactory(project=m.private_project1) + m.private_milestone2 = f.MilestoneFactory(project=m.private_project2) + + return m + + +def test_milestone_retrieve(client, data): + public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) + private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) + private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_milestone_update(client, data): + public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) + private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) + private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + milestone_data = MilestoneSerializer(data.public_milestone).data + milestone_data["name"] = "test" + milestone_data = JSONRenderer().render(milestone_data) + results = helper_test_http_method(client, 'put', public_url, milestone_data, users) + assert results == [401, 403, 403, 200, 200] + + milestone_data = MilestoneSerializer(data.private_milestone1).data + milestone_data["name"] = "test" + milestone_data = JSONRenderer().render(milestone_data) + results = helper_test_http_method(client, 'put', private_url1, milestone_data, users) + assert results == [401, 403, 403, 200, 200] + + milestone_data = MilestoneSerializer(data.private_milestone2).data + milestone_data["name"] = "test" + milestone_data = JSONRenderer().render(milestone_data) + results = helper_test_http_method(client, 'put', private_url2, milestone_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_milestone_delete(client, data): + public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) + private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) + private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_milestone_list(client, data): + url = reverse('milestones-list') + + response = client.get(url) + milestones_data = json.loads(response.content.decode('utf-8')) + assert len(milestones_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + milestones_data = json.loads(response.content.decode('utf-8')) + assert len(milestones_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + milestones_data = json.loads(response.content.decode('utf-8')) + assert len(milestones_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + milestones_data = json.loads(response.content.decode('utf-8')) + assert len(milestones_data) == 3 + assert response.status_code == 200 + + +def test_milestone_create(client, data): + url = reverse('milestones-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "name": "test", + "estimated_start": "2014-12-10", + "estimated_finish": "2014-12-24", + "project": data.public_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Milestone.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "name": "test", + "estimated_start": "2014-12-10", + "estimated_finish": "2014-12-24", + "project": data.private_project1.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Milestone.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "name": "test", + "estimated_start": "2014-12-10", + "estimated_finish": "2014-12-24", + "project": data.private_project2.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Milestone.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + +def test_milestone_patch(client, data): + public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) + private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) + private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"name": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"name": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"name": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + +def test_milestone_action_stats(client, data): + public_url = reverse('milestones-stats', kwargs={"pk": data.public_milestone.pk}) + private_url1 = reverse('milestones-stats', kwargs={"pk": data.private_milestone1.pk}) + private_url2 = reverse('milestones-stats', kwargs={"pk": data.private_milestone2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py new file mode 100644 index 00000000..ca4b1611 --- /dev/null +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -0,0 +1,1556 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.projects import serializers +from taiga.permissions.permissions import MEMBERS_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method + +import json + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_points = f.PointsFactory(project=m.public_project) + m.private_points1 = f.PointsFactory(project=m.private_project1) + m.private_points2 = f.PointsFactory(project=m.private_project2) + + m.public_user_story_status = f.UserStoryStatusFactory(project=m.public_project) + m.private_user_story_status1 = f.UserStoryStatusFactory(project=m.private_project1) + m.private_user_story_status2 = f.UserStoryStatusFactory(project=m.private_project2) + + m.public_task_status = f.TaskStatusFactory(project=m.public_project) + m.private_task_status1 = f.TaskStatusFactory(project=m.private_project1) + m.private_task_status2 = f.TaskStatusFactory(project=m.private_project2) + + m.public_issue_status = f.IssueStatusFactory(project=m.public_project) + m.private_issue_status1 = f.IssueStatusFactory(project=m.private_project1) + m.private_issue_status2 = f.IssueStatusFactory(project=m.private_project2) + + m.public_issue_type = f.IssueTypeFactory(project=m.public_project) + m.private_issue_type1 = f.IssueTypeFactory(project=m.private_project1) + m.private_issue_type2 = f.IssueTypeFactory(project=m.private_project2) + + m.public_priority = f.PriorityFactory(project=m.public_project) + m.private_priority1 = f.PriorityFactory(project=m.private_project1) + m.private_priority2 = f.PriorityFactory(project=m.private_project2) + + m.public_severity = f.SeverityFactory(project=m.public_project) + m.private_severity1 = f.SeverityFactory(project=m.private_project1) + m.private_severity2 = f.SeverityFactory(project=m.private_project2) + + m.project_template = m.public_project.creation_template + + return m + + +def test_roles_retrieve(client, data): + public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) + private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) + private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_roles_update(client, data): + public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) + private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) + private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + role_data = serializers.RoleSerializer(data.public_project.roles.all()[0]).data + role_data["name"] = "test" + role_data = JSONRenderer().render(role_data) + results = helper_test_http_method(client, 'put', public_url, role_data, users) + assert results == [401, 403, 403, 403, 200] + + role_data = serializers.RoleSerializer(data.private_project1.roles.all()[0]).data + role_data["name"] = "test" + role_data = JSONRenderer().render(role_data) + results = helper_test_http_method(client, 'put', private1_url, role_data, users) + assert results == [401, 403, 403, 403, 200] + + role_data = serializers.RoleSerializer(data.private_project2.roles.all()[0]).data + role_data["name"] = "test" + role_data = JSONRenderer().render(role_data) + results = helper_test_http_method(client, 'put', private2_url, role_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_roles_delete(client, data): + public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) + private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) + private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_roles_list(client, data): + url = reverse('roles-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 5 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 5 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 5 + assert response.status_code == 200 + + +def test_roles_patch(client, data): + public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) + private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) + private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_points_retrieve(client, data): + public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) + private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) + private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_points_update(client, data): + public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) + private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) + private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + points_data = serializers.PointsSerializer(data.public_points).data + points_data["name"] = "test" + points_data = JSONRenderer().render(points_data) + results = helper_test_http_method(client, 'put', public_url, points_data, users) + assert results == [401, 403, 403, 403, 200] + + points_data = serializers.PointsSerializer(data.private_points1).data + points_data["name"] = "test" + points_data = JSONRenderer().render(points_data) + results = helper_test_http_method(client, 'put', private1_url, points_data, users) + assert results == [401, 403, 403, 403, 200] + + points_data = serializers.PointsSerializer(data.private_points2).data + points_data["name"] = "test" + points_data = JSONRenderer().render(points_data) + results = helper_test_http_method(client, 'put', private2_url, points_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_points_delete(client, data): + public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) + private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) + private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_points_list(client, data): + url = reverse('points-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_points_patch(client, data): + public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) + private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) + private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_points_action_bulk_update_order(client, data): + url = reverse('points-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_points": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_points": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_points": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +def test_user_story_status_retrieve(client, data): + public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) + private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) + private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_status_update(client, data): + public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) + private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) + private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + user_story_status_data = serializers.UserStoryStatusSerializer(data.public_user_story_status).data + user_story_status_data["name"] = "test" + user_story_status_data = JSONRenderer().render(user_story_status_data) + results = helper_test_http_method(client, 'put', public_url, user_story_status_data, users) + assert results == [401, 403, 403, 403, 200] + + user_story_status_data = serializers.UserStoryStatusSerializer(data.private_user_story_status1).data + user_story_status_data["name"] = "test" + user_story_status_data = JSONRenderer().render(user_story_status_data) + results = helper_test_http_method(client, 'put', private1_url, user_story_status_data, users) + assert results == [401, 403, 403, 403, 200] + + user_story_status_data = serializers.UserStoryStatusSerializer(data.private_user_story_status2).data + user_story_status_data["name"] = "test" + user_story_status_data = JSONRenderer().render(user_story_status_data) + results = helper_test_http_method(client, 'put', private2_url, user_story_status_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_user_story_status_delete(client, data): + public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) + private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) + private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_user_story_status_list(client, data): + url = reverse('userstory-statuses-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_user_story_status_patch(client, data): + public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) + private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) + private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_user_story_status_action_bulk_update_order(client, data): + url = reverse('userstory-statuses-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_userstory_statuses": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_userstory_statuses": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_userstory_statuses": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +def test_task_status_retrieve(client, data): + public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) + private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) + private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_status_update(client, data): + public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) + private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) + private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + task_status_data = serializers.TaskStatusSerializer(data.public_task_status).data + task_status_data["name"] = "test" + task_status_data = JSONRenderer().render(task_status_data) + results = helper_test_http_method(client, 'put', public_url, task_status_data, users) + assert results == [401, 403, 403, 403, 200] + + task_status_data = serializers.TaskStatusSerializer(data.private_task_status1).data + task_status_data["name"] = "test" + task_status_data = JSONRenderer().render(task_status_data) + results = helper_test_http_method(client, 'put', private1_url, task_status_data, users) + assert results == [401, 403, 403, 403, 200] + + task_status_data = serializers.TaskStatusSerializer(data.private_task_status2).data + task_status_data["name"] = "test" + task_status_data = JSONRenderer().render(task_status_data) + results = helper_test_http_method(client, 'put', private2_url, task_status_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_task_status_delete(client, data): + public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) + private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) + private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_task_status_list(client, data): + url = reverse('task-statuses-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_task_status_patch(client, data): + public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) + private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) + private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_task_status_action_bulk_update_order(client, data): + url = reverse('task-statuses-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_task_statuses": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_task_statuses": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_task_statuses": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +def test_issue_status_retrieve(client, data): + public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) + private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) + private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_status_update(client, data): + public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) + private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) + private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + issue_status_data = serializers.IssueStatusSerializer(data.public_issue_status).data + issue_status_data["name"] = "test" + issue_status_data = JSONRenderer().render(issue_status_data) + results = helper_test_http_method(client, 'put', public_url, issue_status_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_status_data = serializers.IssueStatusSerializer(data.private_issue_status1).data + issue_status_data["name"] = "test" + issue_status_data = JSONRenderer().render(issue_status_data) + results = helper_test_http_method(client, 'put', private1_url, issue_status_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_status_data = serializers.IssueStatusSerializer(data.private_issue_status2).data + issue_status_data["name"] = "test" + issue_status_data = JSONRenderer().render(issue_status_data) + results = helper_test_http_method(client, 'put', private2_url, issue_status_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_status_delete(client, data): + public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) + private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) + private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_issue_status_list(client, data): + url = reverse('issue-statuses-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_issue_status_patch(client, data): + public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) + private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) + private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_status_action_bulk_update_order(client, data): + url = reverse('issue-statuses-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_issue_statuses": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_statuses": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_statuses": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +def test_issue_type_retrieve(client, data): + public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) + private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) + private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_type_update(client, data): + public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) + private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) + private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + issue_type_data = serializers.IssueTypeSerializer(data.public_issue_type).data + issue_type_data["name"] = "test" + issue_type_data = JSONRenderer().render(issue_type_data) + results = helper_test_http_method(client, 'put', public_url, issue_type_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_type_data = serializers.IssueTypeSerializer(data.private_issue_type1).data + issue_type_data["name"] = "test" + issue_type_data = JSONRenderer().render(issue_type_data) + results = helper_test_http_method(client, 'put', private1_url, issue_type_data, users) + assert results == [401, 403, 403, 403, 200] + + issue_type_data = serializers.IssueTypeSerializer(data.private_issue_type2).data + issue_type_data["name"] = "test" + issue_type_data = JSONRenderer().render(issue_type_data) + results = helper_test_http_method(client, 'put', private2_url, issue_type_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_type_delete(client, data): + public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) + private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) + private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_issue_type_list(client, data): + url = reverse('issue-types-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_issue_type_patch(client, data): + public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) + private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) + private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_issue_type_action_bulk_update_order(client, data): + url = reverse('issue-types-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_issue_types": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_types": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_issue_types": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +def test_priority_retrieve(client, data): + public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) + private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) + private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_priority_update(client, data): + public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) + private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) + private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + priority_data = serializers.PrioritySerializer(data.public_priority).data + priority_data["name"] = "test" + priority_data = JSONRenderer().render(priority_data) + results = helper_test_http_method(client, 'put', public_url, priority_data, users) + assert results == [401, 403, 403, 403, 200] + + priority_data = serializers.PrioritySerializer(data.private_priority1).data + priority_data["name"] = "test" + priority_data = JSONRenderer().render(priority_data) + results = helper_test_http_method(client, 'put', private1_url, priority_data, users) + assert results == [401, 403, 403, 403, 200] + + priority_data = serializers.PrioritySerializer(data.private_priority2).data + priority_data["name"] = "test" + priority_data = JSONRenderer().render(priority_data) + results = helper_test_http_method(client, 'put', private2_url, priority_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_priority_delete(client, data): + public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) + private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) + private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_priority_list(client, data): + url = reverse('priorities-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_priority_patch(client, data): + public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) + private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) + private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_priority_action_bulk_update_order(client, data): + url = reverse('priorities-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_priorities": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_priorities": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_priorities": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +def test_severity_retrieve(client, data): + public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) + private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) + private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_severity_update(client, data): + public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) + private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) + private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + severity_data = serializers.SeveritySerializer(data.public_severity).data + severity_data["name"] = "test" + severity_data = JSONRenderer().render(severity_data) + results = helper_test_http_method(client, 'put', public_url, severity_data, users) + assert results == [401, 403, 403, 403, 200] + + severity_data = serializers.SeveritySerializer(data.private_severity1).data + severity_data["name"] = "test" + severity_data = JSONRenderer().render(severity_data) + results = helper_test_http_method(client, 'put', private1_url, severity_data, users) + assert results == [401, 403, 403, 403, 200] + + severity_data = serializers.SeveritySerializer(data.private_severity2).data + severity_data["name"] = "test" + severity_data = JSONRenderer().render(severity_data) + results = helper_test_http_method(client, 'put', private2_url, severity_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_severity_delete(client, data): + public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) + private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) + private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_severity_list(client, data): + url = reverse('severities-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_severity_patch(client, data): + public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) + private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) + private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_severity_action_bulk_update_order(client, data): + url = reverse('severities-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_severities": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_severities": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_severities": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + +def test_membership_retrieve(client, data): + public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) + private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) + private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_membership_update(client, data): + public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) + private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) + private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + membership_data = serializers.MembershipSerializer(data.public_membership).data + membership_data["token"] = "test" + membership_data = JSONRenderer().render(membership_data) + results = helper_test_http_method(client, 'put', public_url, membership_data, users) + assert results == [401, 403, 403, 403, 200] + + membership_data = serializers.MembershipSerializer(data.private_membership1).data + membership_data["token"] = "test" + membership_data = JSONRenderer().render(membership_data) + results = helper_test_http_method(client, 'put', private1_url, membership_data, users) + assert results == [401, 403, 403, 403, 200] + + membership_data = serializers.MembershipSerializer(data.private_membership2).data + membership_data["token"] = "test" + membership_data = JSONRenderer().render(membership_data) + results = helper_test_http_method(client, 'put', private2_url, membership_data, users) + assert results == [401, 403, 403, 403, 200] + + +def test_membership_delete(client, data): + public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) + private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) + private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + + +def test_membership_list(client, data): + url = reverse('memberships-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 5 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 5 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 5 + assert response.status_code == 200 + + +def test_membership_patch(client, data): + public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) + private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) + private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + + +def test_project_template_retrieve(client, data): + url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk}) + + users = [ + None, + data.registered_user, + data.superuser, + ] + + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [200, 200, 200] + + +def test_project_template_update(client, data): + url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk}) + + users = [ + None, + data.registered_user, + data.superuser, + ] + + project_template_data = serializers.ProjectTemplateSerializer(data.project_template).data + project_template_data["default_owner_role"] = "test" + project_template_data = JSONRenderer().render(project_template_data) + results = helper_test_http_method(client, 'put', url, project_template_data, users) + assert results == [401, 403, 200] + + +def test_project_template_delete(client, data): + url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk}) + + users = [ + None, + data.registered_user, + data.superuser, + ] + + results = helper_test_http_method(client, 'delete', url, None, users) + assert results == [401, 403, 204] + + +def test_project_template_list(client, data): + url = reverse('project-templates-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 1 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 1 + assert response.status_code == 200 + + client.login(data.superuser) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 1 + assert response.status_code == 200 + + +def test_project_template_patch(client, data): + url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk}) + + users = [ + None, + data.registered_user, + data.superuser, + ] + + results = helper_test_http_method(client, 'patch', url, '{"name": "Test"}', users) + assert results == [401, 403, 200] + + +# def test_project_template_action_create_from_project(client, data): +# assert False +# +# diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py new file mode 100644 index 00000000..95ddc11a --- /dev/null +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -0,0 +1,381 @@ +import pytest +from django.core.urlresolvers import reverse +from django.db.models.loading import get_model + +from rest_framework.renderers import JSONRenderer + +from taiga.projects.serializers import ProjectDetailSerializer +from taiga.permissions.permissions import MEMBERS_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, helper_test_http_method_and_count + +import json + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=['view_project'], + public_permissions=['view_project']) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=['view_project'], + public_permissions=['view_project'], + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + f.RoleFactory(project=m.public_project) + + m.membership = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.membership = f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.membership = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.membership = f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + ContentType = get_model("contenttypes", "ContentType") + Project = get_model("projects", "Project") + + project_ct = ContentType.objects.get_for_model(Project) + + f.VoteFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms) + f.VoteFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner) + f.VoteFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms) + f.VoteFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner) + f.VoteFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms) + f.VoteFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner) + + f.VotesFactory(content_type=project_ct, object_id=m.public_project.pk, count=2) + f.VotesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2) + f.VotesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2) + + return m + + +def test_project_retrieve(client, data): + public_url = reverse('projects-detail', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-detail', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 200, 200] + + +def test_project_update(client, data): + url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) + + project_data = ProjectDetailSerializer(data.private_project2).data + project_data["is_private"] = False + project_data = JSONRenderer().render(project_data) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'put', url, project_data, users) + assert results == [401, 403, 403, 200] + + +def test_project_delete(client, data): + url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'delete', url, None, users) + assert results == [401, 403, 403, 204] + + +def test_project_list(client, data): + url = reverse('projects-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 3 + assert response.status_code == 200 + + +def test_project_patch(client, data): + url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + data = json.dumps({"is_private": False}) + results = helper_test_http_method(client, 'patch', url, data, users) + assert results == [401, 403, 403, 200] + + +def test_project_action_stats(client, data): + public_url = reverse('projects-stats', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-stats', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-stats', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +def test_project_action_star(client, data): + public_url = reverse('projects-star', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-star', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-star', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +def test_project_action_unstar(client, data): + public_url = reverse('projects-unstar', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-unstar', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-unstar', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +def test_project_action_issues_stats(client, data): + public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +def test_project_action_issues_filters_data(client, data): + public_url = reverse('projects-issue-filters-data', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-issue-filters-data', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-issue-filters-data', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +def test_project_action_tags(client, data): + public_url = reverse('projects-tags', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-tags', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-tags', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +def test_project_action_fans(client, data): + public_url = reverse('projects-fans', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-fans', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-fans', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', public_url, None, users) + assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)] + results = helper_test_http_method_and_count(client, 'get', private1_url, None, users) + assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)] + results = helper_test_http_method_and_count(client, 'get', private2_url, None, users) + assert results == [(404, 1), (404, 1), (403, 2), (200, 2), (200, 2)] + + +def test_user_action_starred(client, data): + url1 = reverse('users-starred', kwargs={"pk": data.project_member_without_perms.pk}) + url2 = reverse('users-starred', kwargs={"pk": data.project_member_with_perms.pk}) + url3 = reverse('users-starred', kwargs={"pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', url1, None, users) + assert results == [(200, 0), (200, 0), (200, 0), (200, 0), (200, 0)] + results = helper_test_http_method_and_count(client, 'get', url2, None, users) + assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)] + results = helper_test_http_method_and_count(client, 'get', url3, None, users) + assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)] + + +def test_project_action_create_template(client, data): + public_url = reverse('projects-create-template', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-create-template', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-create-template', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + data.superuser, + ] + + template_data = json.dumps({ + "template_name": "test", + "template_description": "test", + }) + + results = helper_test_http_method(client, 'post', public_url, template_data, users) + assert results == [401, 403, 403, 403, 403, 201] + results = helper_test_http_method(client, 'post', private1_url, template_data, users) + assert results == [401, 403, 403, 403, 403, 201] + results = helper_test_http_method(client, 'post', private2_url, template_data, users) + assert results == [404, 404, 403, 403, 403, 201] + + +def test_invitations_list(client, data): + url = reverse('invitations-list') + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [403, 403, 403, 403] + + +def test_invitations_retrieve(client, data): + invitation = f.MembershipFactory(user=None) + + url = reverse('invitations-detail', kwargs={'token': invitation.token}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py new file mode 100644 index 00000000..fb744731 --- /dev/null +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -0,0 +1,108 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner, + slug="public") + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner, + slug="private1") + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + slug="private2") + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.view_only_membership = f.MembershipFactory(project=m.private_project2, + user=m.other_user, + role__project=m.private_project2, + role__permissions=["view_project"]) + + f.UserStoryFactory(project=m.private_project2, ref=1, pk=1) + f.TaskFactory(project=m.private_project2, ref=2, pk=1) + f.IssueFactory(project=m.private_project2, ref=3, pk=1) + m.milestone = f.MilestoneFactory(project=m.private_project2, slug=4, pk=1) + + return m + + +def test_resolver_list(client, data): + url = reverse('resolver-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?project=public".format(url), None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', "{}?project=private1".format(url), None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', "{}?project=private2".format(url), None, users) + assert results == [401, 403, 403, 200, 200] + + client.login(data.other_user) + response = client.get("{}?project=private2&us=1&task=2&issue=3&milestone=4".format(url)) + assert json.loads(response.content.decode('utf-8')) == {"project": data.private_project2.pk} + + client.login(data.project_owner) + response = client.get("{}?project=private2&us=1&task=2&issue=3&milestone=4".format(url)) + assert json.loads(response.content.decode('utf-8')) == {"project": data.private_project2.pk, "us": 1, "task": 1, "issue": 1, "milestone": data.milestone.pk} diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py new file mode 100644 index 00000000..fd4f400b --- /dev/null +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -0,0 +1,109 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.projects.issues.serializers import IssueSerializer +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method_and_keys, disconnect_signals, reconnect_signals +from taiga.projects.votes.services import add_vote + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_issue = f.IssueFactory(project=m.public_project, + status__project=m.public_project, + severity__project=m.public_project, + priority__project=m.public_project, + type__project=m.public_project, + milestone__project=m.public_project) + m.private_issue1 = f.IssueFactory(project=m.private_project1, + status__project=m.private_project1, + severity__project=m.private_project1, + priority__project=m.private_project1, + type__project=m.private_project1, + milestone__project=m.private_project1) + m.private_issue2 = f.IssueFactory(project=m.private_project2, + status__project=m.private_project2, + severity__project=m.private_project2, + priority__project=m.private_project2, + type__project=m.private_project2, + milestone__project=m.private_project2) + + return m + + +def test_search_list(client, data): + url = reverse('search-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_keys(client, 'get', url, {'project': data.public_project.pk}, users) + all_keys = set(['count', 'userstories', 'issues', 'tasks', 'wikipages']) + assert results == [(200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys)] + results = helper_test_http_method_and_keys(client, 'get', url, {'project': data.private_project1.pk}, users) + assert results == [(200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys)] + results = helper_test_http_method_and_keys(client, 'get', url, {'project': data.private_project2.pk}, users) + assert results == [(200, set(['count'])), (200, set(['count'])), (200, set(['count'])), (200, all_keys), (200, all_keys)] diff --git a/tests/integration/resources_permissions/test_storage_resources.py b/tests/integration/resources_permissions/test_storage_resources.py new file mode 100644 index 00000000..8a38f888 --- /dev/null +++ b/tests/integration/resources_permissions/test_storage_resources.py @@ -0,0 +1,125 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.userstorage.serializers import StorageEntrySerializer +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, helper_test_http_method_and_count, disconnect_signals, reconnect_signals +from taiga.projects.votes.services import add_vote + +from taiga.userstorage.models import StorageEntry + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.user1 = f.UserFactory.create() + m.user2 = f.UserFactory.create() + + m.storage_user1 = f.StorageEntryFactory(owner=m.user1) + m.storage_user2 = f.StorageEntryFactory(owner=m.user2) + m.storage2_user2 = f.StorageEntryFactory(owner=m.user2) + + return m + + +def test_storage_retrieve(client, data): + url = reverse('user-storage-detail', kwargs={"key": data.storage_user1.key}) + + users = [ + None, + data.user1, + data.user2, + ] + + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [401, 200, 404] + + +def test_storage_update(client, data): + url = reverse('user-storage-detail', kwargs={"key": data.storage_user1.key}) + + users = [ + None, + data.user1, + data.user2, + ] + + storage_data = StorageEntrySerializer(data.storage_user1).data + storage_data["key"] = "test" + storage_data = JSONRenderer().render(storage_data) + results = helper_test_http_method(client, 'put', url, storage_data, users) + assert results == [401, 200, 201] + + +def test_storage_delete(client, data): + url = reverse('user-storage-detail', kwargs={"key": data.storage_user1.key}) + + users = [ + None, + data.user1, + data.user2, + ] + + results = helper_test_http_method(client, 'delete', url, None, users) + assert results == [401, 204, 404] + + +def test_storage_list(client, data): + url = reverse('user-storage-list') + + users = [ + None, + data.user1, + data.user2, + ] + + results = helper_test_http_method_and_count(client, 'get', url, None, users) + assert results == [(200, 0), (200, 1), (200, 2)] + + +def test_storage_create(client, data): + url = reverse('user-storage-list') + + users = [ + None, + data.user1, + data.user2, + ] + + create_data = json.dumps({ + "key": "test", + "value": "test", + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: StorageEntry.objects.all().delete()) + assert results == [401, 201, 201] + + +def test_storage_patch(client, data): + url = reverse('user-storage-detail', kwargs={"key": data.storage_user1.key}) + + users = [ + None, + data.user1, + data.user2, + ] + + patch_data = json.dumps({"value": "test"}) + results = helper_test_http_method(client, 'patch', url, patch_data, users) + assert results == [401, 200, 201] diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py new file mode 100644 index 00000000..b642d7ee --- /dev/null +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -0,0 +1,291 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.projects.tasks.serializers import TaskSerializer +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_task = f.TaskFactory(project=m.public_project, + status__project=m.public_project, + milestone__project=m.public_project, + user_story__project=m.public_project) + m.private_task1 = f.TaskFactory(project=m.private_project1, + status__project=m.private_project1, + milestone__project=m.private_project1, + user_story__project=m.private_project1) + m.private_task2 = f.TaskFactory(project=m.private_project2, + status__project=m.private_project2, + milestone__project=m.private_project2, + user_story__project=m.private_project2) + + m.public_project.default_task_status = m.public_task.status + m.public_project.save() + m.private_project1.default_task_status = m.private_task1.status + m.private_project1.save() + m.private_project2.default_task_status = m.private_task2.status + m.private_project2.save() + + return m + + +def test_task_retrieve(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_update(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + task_data = TaskSerializer(data.public_task).data + task_data["subject"] = "test" + task_data = JSONRenderer().render(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task1).data + task_data["subject"] = "test" + task_data = JSONRenderer().render(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task2).data + task_data["subject"] = "test" + task_data = JSONRenderer().render(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_delete(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_task_list(client, data): + url = reverse('tasks-list') + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 3 + assert response.status_code == 200 + + +def test_task_create(client, data): + url = reverse('tasks-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "status": data.public_project.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "status": data.private_project1.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "status": data.private_project2.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + +def test_task_patch(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"subject": "test", "version": data.public_task.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_task1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_task2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + +def test_task_action_bulk_create(client, data): + url = reverse('tasks-bulk-create') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + bulk_data = json.dumps({ + "bulkTasks": "test1\ntest2", + "usId": data.public_task.user_story.pk, + "projectId": data.public_task.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulkTasks": "test1\ntest2", + "usId": data.private_task1.user_story.pk, + "projectId": data.private_task1.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulkTasks": "test1\ntest2", + "usId": data.private_task2.user_story.pk, + "projectId": data.private_task2.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py new file mode 100644 index 00000000..2dbe7074 --- /dev/null +++ b/tests/integration/resources_permissions/test_timelines_resources.py @@ -0,0 +1,104 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + return m + + +def test_user_timeline_retrieve(client, data): + url = reverse('user-timeline-detail', kwargs={"pk": data.registered_user.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [200, 200, 200, 200, 200] + + +def test_project_timeline_retrieve(client, data): + public_url = reverse('project-timeline-detail', kwargs={"pk": data.public_project.pk}) + private_url1 = reverse('project-timeline-detail', kwargs={"pk": data.private_project1.pk}) + private_url2 = reverse('project-timeline-detail', kwargs={"pk": data.private_project2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py new file mode 100644 index 00000000..4eb96b3b --- /dev/null +++ b/tests/integration/resources_permissions/test_users_resources.py @@ -0,0 +1,230 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.users.serializers import UserSerializer +from taiga.users.models import User +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + return m + + +def test_user_retrieve(client, data): + url = reverse('users-detail', kwargs={"pk": data.registered_user.pk}) + + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [200, 200, 200, 200] + + +def test_user_update(client, data): + url = reverse('users-detail', kwargs={"pk": data.registered_user.pk}) + + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + + user_data = UserSerializer(data.registered_user).data + user_data["full_name"] = "test" + user_data = JSONRenderer().render(user_data) + results = helper_test_http_method(client, 'put', url, user_data, users) + assert results == [401, 200, 403, 200] + + +def test_user_delete(client, data): + url = reverse('users-detail', kwargs={"pk": data.registered_user.pk}) + + users = [ + None, + data.other_user, + data.registered_user, + ] + + results = helper_test_http_method(client, 'delete', url, None, users) + assert results == [401, 403, 204] + + +def test_user_list(client, data): + url = reverse('users-list') + + response = client.get(url) + users_data = json.loads(response.content.decode('utf-8')) + assert len(users_data) == 3 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + users_data = json.loads(response.content.decode('utf-8')) + assert len(users_data) == 3 + assert response.status_code == 200 + + client.login(data.other_user) + + response = client.get(url) + users_data = json.loads(response.content.decode('utf-8')) + assert len(users_data) == 3 + assert response.status_code == 200 + + client.login(data.superuser) + + response = client.get(url) + users_data = json.loads(response.content.decode('utf-8')) + assert len(users_data) == 3 + assert response.status_code == 200 + + +def test_user_create(client, data): + url = reverse('users-list') + + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + + create_data = json.dumps({ + "username": "test", + "full_name": "test", + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: User.objects.filter(username="test").delete()) + assert results == [201, 201, 201, 201] + + +def test_user_patch(client, data): + url = reverse('users-detail', kwargs={"pk": data.registered_user.pk}) + + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + + patch_data = json.dumps({"full_name": "test"}) + results = helper_test_http_method(client, 'patch', url, patch_data, users) + assert results == [401, 200, 403, 200] + +def test_user_action_change_password(client, data): + url = reverse('users-change-password') + + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + + patch_data = json.dumps({"password": "test-password"}) + results = helper_test_http_method(client, 'post', url, patch_data, users) + assert results == [401, 204, 204, 204] + +def test_user_action_change_password_from_recovery(client, data): + url = reverse('users-change-password-from-recovery') + + new_user = f.UserFactory(token="test-token") + + def reset_token(): + new_user.token = "test-token" + new_user.save() + + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + + patch_data = json.dumps({"password": "test-password", "token": "test-token"}) + results = helper_test_http_method(client, 'post', url, patch_data, users, reset_token) + assert results == [204, 204, 204, 204] + +def test_user_action_password_recovery(client, data): + url = reverse('users-password-recovery') + + new_user = f.UserFactory.create(username="test") + + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + + patch_data = json.dumps({"username": "test"}) + results = helper_test_http_method(client, 'post', url, patch_data, users) + assert results == [200, 200, 200, 200] + +# def test_membership_retrieve(client, data): +# assert False +# +# +# def test_membership_update(client, data): +# assert False +# +# +# def test_membership_delete(client, data): +# assert False +# +# +# def test_membership_list(client, data): +# assert False +# +# +# def test_membership_patch(client, data): +# assert False +# +# +# def test_invitation_retrieve(client, data): +# assert False +# +# +# def test_invitation_update(client, data): +# assert False +# +# +# def test_invitation_delete(client, data): +# assert False +# +# +# def test_invitation_list(client, data): +# assert False +# +# +# def test_invitation_patch(client, data): +# assert False diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py new file mode 100644 index 00000000..54fce096 --- /dev/null +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -0,0 +1,296 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.projects.userstories.serializers import UserStorySerializer +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_points = f.PointsFactory(project=m.public_project) + m.private_points1 = f.PointsFactory(project=m.private_project1) + m.private_points2 = f.PointsFactory(project=m.private_project2) + + m.public_role_points = f.RolePointsFactory(role=m.public_project.roles.all()[0], + points=m.public_points, + user_story__project=m.public_project) + m.private_role_points1 = f.RolePointsFactory(role=m.private_project1.roles.all()[0], + points=m.private_points1, + user_story__project=m.private_project1) + m.private_role_points2 = f.RolePointsFactory(role=m.private_project2.roles.all()[0], + points=m.private_points2, + user_story__project=m.private_project2) + + m.public_user_story = m.public_role_points.user_story + m.private_user_story1 = m.private_role_points1.user_story + m.private_user_story2 = m.private_role_points2.user_story + + return m + + +def test_user_story_retrieve(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_update(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + user_story_data = UserStorySerializer(data.public_user_story).data + user_story_data["subject"] = "test" + user_story_data = JSONRenderer().render(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story1).data + user_story_data["subject"] = "test" + user_story_data = JSONRenderer().render(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story2).data + user_story_data["subject"] = "test" + user_story_data = JSONRenderer().render(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_delete(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_user_story_list(client, data): + url = reverse('userstories-list') + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 3 + assert response.status_code == 200 + + +def test_user_story_create(client, data): + url = reverse('userstories-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + +def test_user_story_patch(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"subject": "test", "version": data.public_user_story.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_user_story1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_user_story2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_action_bulk_create(client, data): + url = reverse('userstories-bulk-create') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + bulk_data = json.dumps({"bulkStories": "test1\ntest2", "projectId": data.public_user_story.project.pk}) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 200, 200, 200, 200] + + bulk_data = json.dumps({"bulkStories": "test1\ntest2", "projectId": data.private_user_story1.project.pk}) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 200, 200, 200, 200] + + bulk_data = json.dumps({"bulkStories": "test1\ntest2", "projectId": data.private_user_story2.project.pk}) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_action_bulk_update_order(client, data): + url = reverse('userstories-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulkStories": [(1,2)], + "projectId": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 204, 204] + + post_data = json.dumps({ + "bulkStories": [(1,2)], + "projectId": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 204, 204] + + post_data = json.dumps({ + "bulkStories": [(1,2)], + "projectId": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 204, 204] diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py new file mode 100644 index 00000000..1ee4b580 --- /dev/null +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -0,0 +1,422 @@ +import pytest +from django.core.urlresolvers import reverse + +from rest_framework.renderers import JSONRenderer + +from taiga.projects.wiki.serializers import WikiPageSerializer, WikiLinkSerializer +from taiga.projects.wiki.models import WikiPage, WikiLink +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from taiga.projects.votes.services import add_vote + +import json + +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + + m.public_wiki_page = f.WikiPageFactory(project=m.public_project) + m.private_wiki_page1 = f.WikiPageFactory(project=m.private_project1) + m.private_wiki_page2 = f.WikiPageFactory(project=m.private_project2) + + m.public_wiki_link = f.WikiLinkFactory(project=m.public_project) + m.private_wiki_link1 = f.WikiLinkFactory(project=m.private_project1) + m.private_wiki_link2 = f.WikiLinkFactory(project=m.private_project2) + + return m + + +def test_wiki_page_retrieve(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_page_update(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + wiki_page_data = WikiPageSerializer(data.public_wiki_page).data + wiki_page_data["content"] = "test" + wiki_page_data = JSONRenderer().render(wiki_page_data) + results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) + assert results == [401, 200, 200, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data + wiki_page_data["content"] = "test" + wiki_page_data = JSONRenderer().render(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) + assert results == [401, 200, 200, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data + wiki_page_data["content"] = "test" + wiki_page_data = JSONRenderer().render(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_page_delete(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_wiki_page_list(client, data): + url = reverse('wiki-list') + + response = client.get(url) + wiki_pages_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_pages_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + wiki_pages_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_pages_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + wiki_pages_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_pages_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + wiki_pages_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_pages_data) == 3 + assert response.status_code == 200 + + +def test_wiki_page_create(client, data): + url = reverse('wiki-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.public_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.private_project1.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.private_project2.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + +def test_wiki_page_patch(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 200, 200, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 200, 200, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + +def test_wiki_page_action_render(client, data): + url = reverse('wiki-render') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({"content": "test", "project_id": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [200, 200, 200, 200, 200] + + +def test_wiki_link_retrieve(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_link_update(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + wiki_link_data = WikiLinkSerializer(data.public_wiki_link).data + wiki_link_data["title"] = "test" + wiki_link_data = JSONRenderer().render(wiki_link_data) + results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users) + assert results == [401, 200, 200, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data + wiki_link_data["title"] = "test" + wiki_link_data = JSONRenderer().render(wiki_link_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users) + assert results == [401, 200, 200, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data + wiki_link_data["title"] = "test" + wiki_link_data = JSONRenderer().render(wiki_link_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_link_delete(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + +def test_wiki_link_list(client, data): + url = reverse('wiki-links-list') + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 3 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 3 + assert response.status_code == 200 + + +def test_wiki_link_create(client, data): + url = reverse('wiki-links-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.public_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.private_project1.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 201, 201, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.private_project2.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + +def test_wiki_link_patch(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 200, 200, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 200, 200, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py index 77ffcf69..72d8ff17 100644 --- a/tests/integration/test_attachments.py +++ b/tests/integration/test_attachments.py @@ -10,7 +10,7 @@ pytestmark = pytest.mark.django_db def test_authentication(client): "User can't access an attachment if not authenticated" - attachment = f.AttachmentFactory.create() + attachment = f.UserStoryAttachmentFactory.create() url = reverse("attachment-url", kwargs={"pk": attachment.pk}) response = client.get(url) @@ -20,7 +20,7 @@ def test_authentication(client): def test_authorization(client): "User can't access an attachment if not authorized" - attachment = f.AttachmentFactory.create() + attachment = f.UserStoryAttachmentFactory.create() user = f.UserFactory.create() url = reverse("attachment-url", kwargs={"pk": attachment.pk}) @@ -34,7 +34,7 @@ def test_authorization(client): @set_settings(IN_DEVELOPMENT_SERVER=True) def test_attachment_redirect_in_devserver(client): "When accessing the attachment in devserver redirect to the real attachment url" - attachment = f.AttachmentFactory.create() + attachment = f.UserStoryAttachmentFactory.create(attached_file="test") url = reverse("attachment-url", kwargs={"pk": attachment.pk}) @@ -47,7 +47,7 @@ def test_attachment_redirect_in_devserver(client): @set_settings(IN_DEVELOPMENT_SERVER=False) def test_attachment_redirect(client): "When accessing the attachment redirect using X-Accel-Redirect header" - attachment = f.AttachmentFactory.create() + attachment = f.UserStoryAttachmentFactory.create() url = reverse("attachment-url", kwargs={"pk": attachment.pk}) diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py new file mode 100644 index 00000000..4e337e43 --- /dev/null +++ b/tests/integration/test_permissions.py @@ -0,0 +1,116 @@ +import pytest + +from taiga.permissions import service, permissions +from django.contrib.auth.models import AnonymousUser + +from .. import factories + +pytestmark = pytest.mark.django_db + + +def test_get_user_project_role(): + user1 = factories.UserFactory() + user2 = factories.UserFactory() + project = factories.ProjectFactory() + role = factories.RoleFactory() + membership = factories.MembershipFactory(user=user1, project=project, role=role) + + assert service._get_user_project_membership(user1, project) == membership + assert service._get_user_project_membership(user2, project) is None + + +def test_anon_get_user_project_permissions(): + project = factories.ProjectFactory() + project.anon_permissions = ["test1"] + project.public_permissions = ["test2"] + assert service.get_user_project_permissions(AnonymousUser(), project) == set(["test1"]) + + +def test_user_get_user_project_permissions_on_public_project(): + user1 = factories.UserFactory() + project = factories.ProjectFactory() + project.anon_permissions = ["test1"] + project.public_permissions = ["test2"] + assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"]) + + +def test_user_get_user_project_permissions_on_private_project(): + user1 = factories.UserFactory() + project = factories.ProjectFactory() + project.anon_permissions = ["test1"] + project.public_permissions = ["test2"] + project.is_private = True + assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"]) + + +def test_owner_get_user_project_permissions(): + user1 = factories.UserFactory() + project = factories.ProjectFactory() + project.anon_permissions = ["test1"] + project.public_permissions = ["test2"] + project.owner = user1 + role = factories.RoleFactory(permissions=["view_us"]) + factories.MembershipFactory(user=user1, project=project, role=role) + + expected_perms = set( + ["test1", "test2"] + + [x[0] for x in permissions.OWNERS_PERMISSIONS] + + [x[0] for x in permissions.MEMBERS_PERMISSIONS] + ) + assert service.get_user_project_permissions(user1, project) == expected_perms + + +def test_owner_member_get_user_project_permissions(): + user1 = factories.UserFactory() + project = factories.ProjectFactory() + project.anon_permissions = ["test1"] + project.public_permissions = ["test2"] + role = factories.RoleFactory(permissions=["test3"]) + factories.MembershipFactory(user=user1, project=project, role=role, is_owner=True) + + expected_perms = set( + ["test1", "test2", "test3"] + + [x[0] for x in permissions.OWNERS_PERMISSIONS] + ) + assert service.get_user_project_permissions(user1, project) == expected_perms + + +def test_member_get_user_project_permissions(): + user1 = factories.UserFactory() + project = factories.ProjectFactory() + project.anon_permissions = ["test1"] + project.public_permissions = ["test2"] + role = factories.RoleFactory(permissions=["test3"]) + factories.MembershipFactory(user=user1, project=project, role=role) + + assert service.get_user_project_permissions(user1, project) == set(["test1", "test2", "test3"]) + + +def test_anon_user_has_perm(): + project = factories.ProjectFactory() + project.anon_permissions = ["test"] + assert service.user_has_perm(AnonymousUser(), "test", project) == True + assert service.user_has_perm(AnonymousUser(), "fail", project) == False + + +def test_authenticated_user_has_perm_on_project(): + user1 = factories.UserFactory() + project = factories.ProjectFactory() + project.public_permissions = ["test"] + assert service.user_has_perm(user1, "test", project) == True + assert service.user_has_perm(user1, "fail", project) == False + + +def test_authenticated_user_has_perm_on_project_related_object(): + user1 = factories.UserFactory() + project = factories.ProjectFactory() + project.public_permissions = ["test"] + us = factories.UserStoryFactory(project=project) + + assert service.user_has_perm(user1, "test", us) == True + assert service.user_has_perm(user1, "fail", us) == False + + +def test_authenticated_user_has_perm_on_invalid_object(): + user1 = factories.UserFactory() + assert service.user_has_perm(user1, "test", user1) == False diff --git a/tests/integration/test_project_us.py b/tests/integration/test_project_us.py index d2a4844f..c369daa7 100644 --- a/tests/integration/test_project_us.py +++ b/tests/integration/test_project_us.py @@ -28,9 +28,9 @@ pytestmark = pytest.mark.django_db def test_archived_filter(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - membership = f.MembershipFactory.create(project=project, user=user) - userstory_1 = f.UserStoryFactory.create(project=project) - user_story_2 = f.UserStoryFactory.create(is_archived=True, project=project) + f.MembershipFactory.create(project=project, user=user) + f.UserStoryFactory.create(project=project) + f.UserStoryFactory.create(is_archived=True, project=project) client.login(user) diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py index 72986025..2eeecf68 100644 --- a/tests/integration/test_searches.py +++ b/tests/integration/test_searches.py @@ -21,9 +21,21 @@ from django.core.urlresolvers import reverse from .. import factories as f +from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from tests.utils import disconnect_signals, reconnect_signals + pytestmark = pytest.mark.django_db + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + @pytest.fixture def searches_initial_data(): m = type("InitialData", (object,), {})() @@ -31,12 +43,33 @@ def searches_initial_data(): m.project1 = f.ProjectFactory.create() m.project2 = f.ProjectFactory.create() - m.member1 = f.MembershipFactory.create(project=m.project1) - m.member2 = f.MembershipFactory.create(project=m.project1) + m.member1 = f.MembershipFactory(project=m.project1, + role__project=m.project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.member2 = f.MembershipFactory(project=m.project1, + role__project=m.project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + f.RoleFactory(project=m.project2) + + m.points1 = f.PointsFactory(project=m.project1, value=None) + m.points2 = f.PointsFactory(project=m.project2, value=None) + + m.role_points1 = f.RolePointsFactory.create(role=m.project1.roles.all()[0], + points=m.points1, + user_story__project=m.project1) + m.role_points2 = f.RolePointsFactory.create(role=m.project1.roles.all()[0], + points=m.points1, + user_story__project=m.project1, + user_story__description="Back to the future") + m.role_points3 = f.RolePointsFactory.create(role=m.project2.roles.all()[0], + points=m.points2, + user_story__project=m.project2) + + m.us1 = m.role_points1.user_story + m.us2 = m.role_points2.user_story + m.us3 = m.role_points3.user_story - m.us1 = f.UserStoryFactory.create(project=m.project1) - m.us2 = f.UserStoryFactory.create(project=m.project1, description="Back to the future") - m.us3 = f.UserStoryFactory.create(project=m.project2) m.tsk1 = f.TaskFactory.create(project=m.project2) m.tsk2 = f.TaskFactory.create(project=m.project1) @@ -73,7 +106,8 @@ def test_search_all_objects_in_project_is_not_mine(client, searches_initial_data client.login(data.member1.user) response = client.get(reverse("search-list"), {"project": data.project2.id}) - assert response.status_code == 403 + assert response.status_code == 200 + assert response.data["count"] == 0 def test_search_text_query_in_my_project(client, searches_initial_data): diff --git a/tests/integration/test_stars.py b/tests/integration/test_stars.py index 67461f45..ce62aed4 100644 --- a/tests/integration/test_stars.py +++ b/tests/integration/test_stars.py @@ -75,7 +75,7 @@ def test_list_project_fans(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) fan = f.VoteFactory.create(content_object=project) - url = reverse("project-fans-list", args=(project.id,)) + url = reverse("projects-fans", args=(project.id,)) client.login(user) response = client.get(url) @@ -84,23 +84,10 @@ def test_list_project_fans(client): assert response.data[0]['id'] == fan.user.id -def test_get_project_fan(client): - user = f.UserFactory.create() - project = f.ProjectFactory.create(owner=user) - fan = f.VoteFactory.create(content_object=project) - url = reverse("project-fans-detail", args=(project.id, fan.user.id)) - - client.login(user) - response = client.get(url) - - assert response.status_code == 200 - assert response.data['id'] == fan.user.id - - def test_list_user_starred_projects(client): user = f.UserFactory.create() project = f.ProjectFactory() - url = reverse("user-starred-list", args=(user.id,)) + url = reverse("users-starred", args=(user.id,)) f.VoteFactory.create(user=user, content_object=project) client.login(user) @@ -110,19 +97,6 @@ def test_list_user_starred_projects(client): assert response.data[0]['id'] == project.id -def test_get_user_starred_project(client): - user = f.UserFactory.create() - project = f.ProjectFactory() - url = reverse("user-starred-detail", args=(user.id, project.id)) - f.VoteFactory.create(user=user, content_object=project) - - client.login(user) - response = client.get(url) - - assert response.status_code == 200 - assert response.data['id'] == project.id - - def test_get_project_stars(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) diff --git a/tests/integration/test_userstorage_api.py b/tests/integration/test_userstorage_api.py index dbb419d7..e0b497c5 100644 --- a/tests/integration/test_userstorage_api.py +++ b/tests/integration/test_userstorage_api.py @@ -25,17 +25,18 @@ from .. import factories pytestmark = pytest.mark.django_db -def test_list_userstories(client): +def test_list_userstorage(client): user1 = factories.UserFactory() user2 = factories.UserFactory() storage11 = factories.StorageEntryFactory(owner=user1) - storage12 = factories.StorageEntryFactory(owner=user1) + factories.StorageEntryFactory(owner=user1) storage13 = factories.StorageEntryFactory(owner=user1) - storage21 = factories.StorageEntryFactory(owner=user2) + factories.StorageEntryFactory(owner=user2) # List by anonumous user response = client.get(reverse("user-storage-list")) - assert response.status_code == 401 + assert response.status_code == 200 + assert len(response.data) == 0 # List own entries client.login(username=user1.username, password=user1.username) @@ -57,8 +58,6 @@ def test_list_userstories(client): assert response.status_code == 200 assert len(response.data) == 2 - client.logout() - def test_view_storage_entries(client): user1 = factories.UserFactory() @@ -84,12 +83,9 @@ def test_view_storage_entries(client): response = client.get(reverse("user-storage-detail", args=["foobar"])) assert response.status_code == 404 - client.logout() - def test_create_entries(client): user1 = factories.UserFactory() - user2 = factories.UserFactory() storage11 = factories.StorageEntryFactory(owner=user1) form = {"key": "foo", @@ -119,12 +115,9 @@ def test_create_entries(client): response = client.post(reverse("user-storage-list"), error_form) assert response.status_code == 400 - client.logout() - def test_update_entries(client): user1 = factories.UserFactory() - user2 = factories.UserFactory() storage11 = factories.StorageEntryFactory(owner=user1) # Update by anonymous user @@ -158,9 +151,6 @@ def test_update_entries(client): assert response.status_code == 200 assert response.data["value"] == form["value"] - client.logout() - - def test_delete_storage_entry(client): user1 = factories.UserFactory() @@ -186,6 +176,3 @@ def test_delete_storage_entry(client): client.login(username=user2.username, password=user2.username) response = client.delete(reverse("user-storage-detail", args=[storage11.key])) assert response.status_code == 404 - - client.logout() - diff --git a/tests/unit/test_base_api_permissions.py b/tests/unit/test_base_api_permissions.py new file mode 100644 index 00000000..8fd74f66 --- /dev/null +++ b/tests/unit/test_base_api_permissions.py @@ -0,0 +1,20 @@ +from taiga.base.api.permissions import (PermissionComponent, + AllowAny as TruePermissionComponent, + DenyAll as FalsePermissionComponent) + +import pytest + + +def test_permission_component_composition(): + assert (TruePermissionComponent() | TruePermissionComponent()).check_permissions(None, None, None) + assert (TruePermissionComponent() | FalsePermissionComponent()).check_permissions(None, None, None) + assert (FalsePermissionComponent() | TruePermissionComponent()).check_permissions(None, None, None) + assert not (FalsePermissionComponent() | FalsePermissionComponent()).check_permissions(None, None, None) + + assert (TruePermissionComponent() & TruePermissionComponent()).check_permissions(None, None, None) + assert not (TruePermissionComponent() & FalsePermissionComponent()).check_permissions(None, None, None) + assert not (FalsePermissionComponent() & TruePermissionComponent()).check_permissions(None, None, None) + assert not (FalsePermissionComponent() & FalsePermissionComponent()).check_permissions(None, None, None) + + assert (~FalsePermissionComponent()).check_permissions(None, None, None) + assert not (~TruePermissionComponent()).check_permissions(None, None, None) diff --git a/tests/unit/test_permissions.py b/tests/unit/test_permissions.py new file mode 100644 index 00000000..025a4bc6 --- /dev/null +++ b/tests/unit/test_permissions.py @@ -0,0 +1,11 @@ +from taiga.permissions import service +from taiga.users.models import Role + +import pytest + + +def test_role_has_perm(): + role = Role() + role.permissions = ["test"] + assert service.role_has_perm(role, "test") + assert service.role_has_perm(role, "false") == False diff --git a/tests/utils.py b/tests/utils.py index 8367b2cf..4537c911 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import functools +import json from django.conf import settings from django.db.models import signals @@ -86,3 +87,36 @@ class SettingsTestCase(object): def teardown_class(cls): override_settings(cls.ORIGINAL_SETTINGS) cls.OVERRIDE_SETTINGS.clear() + +def _helper_test_http_method_responses(client, method, url, data, users, after_each_request=None): + results = [] + for user in users: + if user is None: + client.logout() + else: + client.login(user) + if data: + response = getattr(client, method)(url, data, content_type="application/json") + else: + response = getattr(client, method)(url) + if response.status_code == 400: + print(response.content) + + results.append(response) + + if after_each_request is not None: + after_each_request() + return results + +def helper_test_http_method(client, method, url, data, users, after_each_request=None): + responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request) + return list(map(lambda r: r.status_code, responses)) + + +def helper_test_http_method_and_count(client, method, url, data, users, after_each_request=None): + responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request) + return list(map(lambda r: (r.status_code, len(json.loads(r.content.decode('utf-8')))), responses)) + +def helper_test_http_method_and_keys(client, method, url, data, users, after_each_request=None): + responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request) + return list(map(lambda r: (r.status_code, set(json.loads(r.content.decode('utf-8')).keys())), responses))