diff --git a/bothub/api/routers.py b/bothub/api/routers.py index 07cc8123..952b021c 100644 --- a/bothub/api/routers.py +++ b/bothub/api/routers.py @@ -20,6 +20,9 @@ from .views import Categories from .views import RepositoriesViewSet from .views import TranslationsViewSet +from .views import RepositoryAuthorizationViewSet +from .views import RepositoryAuthorizationRoleViewSet +from .views import SearchUserViewSet class Router(routers.SimpleRouter): @@ -110,3 +113,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): router.register('categories', Categories) router.register('repositories', RepositoriesViewSet) router.register('translations', TranslationsViewSet) +router.register('authorizations', RepositoryAuthorizationViewSet) +router.register('authorization-role', + RepositoryAuthorizationRoleViewSet) +router.register('search-user', SearchUserViewSet) diff --git a/bothub/api/serializers/__init__.py b/bothub/api/serializers/__init__.py index 7d49f723..eb88b23e 100644 --- a/bothub/api/serializers/__init__.py +++ b/bothub/api/serializers/__init__.py @@ -5,6 +5,7 @@ AnalyzeTextSerializer, EditRepositorySerializer, VoteSerializer, + RepositoryAuthorizationRoleSerializer, ) from .category import ( # noqa: F401 diff --git a/bothub/api/serializers/repository.py b/bothub/api/serializers/repository.py index 3edd1f04..e6468390 100644 --- a/bothub/api/serializers/repository.py +++ b/bothub/api/serializers/repository.py @@ -1,4 +1,6 @@ from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied +from django.utils.translation import gettext as _ from bothub.common.models import Repository from bothub.common.models import RepositoryCategory @@ -101,7 +103,9 @@ class Meta: fields = [ 'uuid', 'user', + 'user__nickname', 'repository', + 'role', 'level', 'can_read', 'can_contribute', @@ -110,6 +114,11 @@ class Meta: 'created_at', ] + user__nickname = serializers.SlugRelatedField( + source='user', + slug_field='nickname', + read_only=True) + class AnalyzeTextSerializer(serializers.Serializer): language = serializers.ChoiceField(LANGUAGE_CHOICES, required=True) @@ -122,3 +131,16 @@ class Meta: fields = [ 'vote', ] + + +class RepositoryAuthorizationRoleSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryAuthorization + fields = [ + 'role', + ] + + def validate(self, data): + if self.instance.user == self.instance.repository.owner: + raise PermissionDenied(_('The owner role can\'t be changed.')) + return data diff --git a/bothub/api/serializers/user.py b/bothub/api/serializers/user.py index a0381cf1..7032fdca 100644 --- a/bothub/api/serializers/user.py +++ b/bothub/api/serializers/user.py @@ -35,7 +35,6 @@ class Meta: model = User fields = [ 'nickname', - 'email', 'name', 'locale', ] diff --git a/bothub/api/tests/test_authorization.py b/bothub/api/tests/test_authorization.py index 2c20111d..c3ef7dcd 100644 --- a/bothub/api/tests/test_authorization.py +++ b/bothub/api/tests/test_authorization.py @@ -2,12 +2,16 @@ from django.test import TestCase from django.test import RequestFactory +from django.test.client import MULTIPART_CONTENT from rest_framework import status from bothub.common import languages from bothub.common.models import Repository +from bothub.common.models import RepositoryAuthorization from ..views import RepositoryViewSet +from ..views import RepositoryAuthorizationViewSet +from ..views import RepositoryAuthorizationRoleViewSet from .utils import create_user_and_token @@ -90,3 +94,144 @@ def test_user_forbidden_private(self): self.assertEqual( response.status_code, status.HTTP_403_FORBIDDEN) + + +class ListAuthorizationTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + self.user_auth = self.repository.get_user_authorization(self.user) + self.user_auth.role = RepositoryAuthorization.ROLE_CONTRIBUTOR + self.user_auth.save() + + def request(self, repository, token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.get( + '/api/list-authorizations/', + { + 'repository': repository.uuid, + }, + **authorization_header) + response = RepositoryAuthorizationViewSet.as_view( + {'get': 'list'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + self.repository, + self.owner_token) + + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + + self.assertEqual( + content_data.get('count'), + 1) + + self.assertEqual( + content_data.get('results')[0].get('user'), + self.user.id) + + def test_user_forbidden(self): + response, content_data = self.request( + self.repository, + self.user_token) + + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + +class UpdateAuthorizationRoleTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + def request(self, repository, token, user, data): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.patch( + '/api/authorization-role/{}/{}/'.format( + repository.uuid, user.nickname), + self.factory._encode_data(data, MULTIPART_CONTENT), + MULTIPART_CONTENT, + **authorization_header) + view = RepositoryAuthorizationRoleViewSet.as_view( + {'patch': 'update'}) + response = view( + request, + repository__uuid=repository.uuid, + user__nickname=user.nickname) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + self.repository, + self.owner_token, + self.user, + { + 'role': RepositoryAuthorization.ROLE_CONTRIBUTOR, + }) + + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('role'), + RepositoryAuthorization.ROLE_CONTRIBUTOR) + + user_authorization = self.repository.get_user_authorization(self.user) + self.assertEqual( + user_authorization.role, + RepositoryAuthorization.ROLE_CONTRIBUTOR) + + def test_forbidden(self): + response, content_data = self.request( + self.repository, + self.user_token, + self.user, + { + 'role': RepositoryAuthorization.ROLE_CONTRIBUTOR, + }) + + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_owner_can_t_set_your_role(self): + response, content_data = self.request( + self.repository, + self.owner_token, + self.owner, + { + 'role': RepositoryAuthorization.ROLE_CONTRIBUTOR, + }) + + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/views.py b/bothub/api/views.py index 9bb78d70..46aa5946 100644 --- a/bothub/api/views.py +++ b/bothub/api/views.py @@ -25,6 +25,7 @@ from bothub.common.models import RepositoryTranslatedExampleEntity from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryVote +from bothub.common.models import RepositoryAuthorization from bothub.authentication.models import User from .serializers import RepositorySerializer @@ -45,6 +46,7 @@ from .serializers import EditRepositorySerializer from .serializers import NewRepositoryTranslatedExampleSerializer from .serializers import VoteSerializer +from .serializers import RepositoryAuthorizationRoleSerializer # Permisions @@ -94,6 +96,12 @@ def has_object_permission(self, request, view, obj): return authorization.can_contribute +class RepositoryAdminManagerAuthorization(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + authorization = obj.repository.get_user_authorization(request.user) + return authorization.is_admin + + # Filters class ExamplesFilter(filters.FilterSet): @@ -222,6 +230,31 @@ def filter_to_language(self, queryset, name, value): return queryset.filter(language=value) +class RepositoryAuthorizationFilter(filters.FilterSet): + class Meta: + model = RepositoryAuthorization + fields = ['repository'] + + repository = filters.CharFilter( + name='repository', + method='filter_repository_uuid', + help_text=_('Repository\'s UUID')) + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + if not authorization.is_admin: + raise PermissionDenied() + return queryset.filter(repository=repository) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository UUID')) + + # Mixins class MultipleFieldLookupMixin(object): @@ -753,3 +786,69 @@ class TranslationsViewSet( serializer_class = RepositoryTranslatedExampleSerializer queryset = RepositoryTranslatedExample.objects.all() filter_class = TranslationsFilter + + +class RepositoryAuthorizationViewSet( + mixins.ListModelMixin, + GenericViewSet): + queryset = RepositoryAuthorization.objects.exclude( + role=RepositoryAuthorization.ROLE_NOT_SETTED) + serializer_class = RepositoryAuthorizationSerializer + filter_class = RepositoryAuthorizationFilter + permission_classes = [ + IsAuthenticated, + ] + + +class RepositoryAuthorizationRoleViewSet( + MultipleFieldLookupMixin, + mixins.UpdateModelMixin, + GenericViewSet): + queryset = RepositoryAuthorization.objects.exclude( + role=RepositoryAuthorization.ROLE_NOT_SETTED) + lookup_field = 'user__nickname' + lookup_fields = ['repository__uuid', 'user__nickname'] + serializer_class = RepositoryAuthorizationRoleSerializer + permission_classes = [ + IsAuthenticated, + RepositoryAdminManagerAuthorization, + ] + + def get_object(self): + repository_uuid = self.kwargs.get('repository__uuid') + user_nickname = self.kwargs.get('user__nickname') + + repository = get_object_or_404(Repository, uuid=repository_uuid) + user = get_object_or_404(User, nickname=user_nickname) + + obj = repository.get_user_authorization(user) + + self.check_object_permissions(self.request, obj) + return obj + + +class SearchUserViewSet( + mixins.ListModelMixin, + GenericViewSet): + serializer_class = UserSerializer + queryset = User.objects.all() + filter_backends = [ + DjangoFilterBackend, + SearchFilter, + ] + search_fields = [ + '=name', + '^name', + '$name', + '=nickname', + '^nickname', + '$nickname', + '=email', + ] + pagination_class = None + limit = 5 + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset())[:self.limit] + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) diff --git a/bothub/common/migrations/0011_repositoryauthorization_role.py b/bothub/common/migrations/0011_repositoryauthorization_role.py new file mode 100644 index 00000000..fd96afc8 --- /dev/null +++ b/bothub/common/migrations/0011_repositoryauthorization_role.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2018-06-21 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0010_auto_20180611_1123'), + ] + + operations = [ + migrations.AddField( + model_name='repositoryauthorization', + name='role', + field=models.PositiveIntegerField(choices=[(0, 'not set'), (1, 'user'), (2, 'contributor'), (3, 'admin')], default=0, verbose_name='role'), + ), + ] diff --git a/bothub/common/models.py b/bothub/common/models.py index 67dadb36..a3e165db 100644 --- a/bothub/common/models.py +++ b/bothub/common/models.py @@ -181,9 +181,10 @@ def votes_sum(self): @property def intents(self): return list(set(self.examples( - deleted=False).values_list( - 'intent', - flat=True))) + deleted=False).exclude( + intent='').values_list( + 'intent', + flat=True))) @property def entities(self): @@ -598,7 +599,20 @@ class Meta: LEVEL_NOTHING = 0 LEVEL_READER = 1 - LEVEL_ADMIN = 2 + LEVEL_CONTRIBUTOR = 2 + LEVEL_ADMIN = 3 + + ROLE_NOT_SETTED = 0 + ROLE_USER = 1 + ROLE_CONTRIBUTOR = 2 + ROLE_ADMIN = 3 + + ROLE_CHOICES = [ + (ROLE_NOT_SETTED, _('not set')), + (ROLE_USER, _('user')), + (ROLE_CONTRIBUTOR, _('contributor')), + (ROLE_ADMIN, _('admin')), + ] uuid = models.UUIDField( _('UUID'), @@ -611,6 +625,10 @@ class Meta: repository = models.ForeignKey( Repository, models.CASCADE) + role = models.PositiveIntegerField( + _('role'), + choices=ROLE_CHOICES, + default=ROLE_NOT_SETTED) created_at = models.DateTimeField( _('created at'), auto_now_add=True) @@ -624,20 +642,35 @@ def level(self): if user and self.repository.owner == user: return RepositoryAuthorization.LEVEL_ADMIN - if self.repository.is_private: - return RepositoryAuthorization.LEVEL_NOTHING - return RepositoryAuthorization.LEVEL_READER + + if self.role == RepositoryAuthorization.ROLE_NOT_SETTED: + if self.repository.is_private: + return RepositoryAuthorization.LEVEL_NOTHING + return RepositoryAuthorization.LEVEL_READER + + if self.role == RepositoryAuthorization.ROLE_USER: + return RepositoryAuthorization.LEVEL_READER + + if self.role == RepositoryAuthorization.ROLE_CONTRIBUTOR: + return RepositoryAuthorization.LEVEL_CONTRIBUTOR + + if self.role == RepositoryAuthorization.ROLE_ADMIN: + return RepositoryAuthorization.LEVEL_ADMIN + + return RepositoryAuthorization.LEVEL_NOTHING @property def can_read(self): return self.level in [ RepositoryAuthorization.LEVEL_READER, + RepositoryAuthorization.LEVEL_CONTRIBUTOR, RepositoryAuthorization.LEVEL_ADMIN, ] @property def can_contribute(self): return self.level in [ + RepositoryAuthorization.LEVEL_CONTRIBUTOR, RepositoryAuthorization.LEVEL_ADMIN, ] diff --git a/bothub/common/tests.py b/bothub/common/tests.py index a6b83691..9c0633c7 100644 --- a/bothub/common/tests.py +++ b/bothub/common/tests.py @@ -312,6 +312,16 @@ def test_entities(self): 'name', self.repository.entities) + def test_not_blank_value_in_intents(self): + RepositoryExample.objects.create( + repository_update=self.repository.current_update( + languages.LANGUAGE_EN), + text='hi') + + self.assertNotIn( + '', + self.repository.intents) + class RepositoryExampleTestCase(TestCase): def setUp(self): @@ -451,6 +461,56 @@ def test_is_admin(self): .get_user_authorization(self.user) self.assertFalse(private_authorization_user.is_admin) + def test_owner_ever_admin(self): + authorization_owner = self.repository.get_user_authorization( + self.owner) + self.assertTrue(authorization_owner.is_admin) + + def test_role_user_can_read(self): + # public repository + authorization_user = self.repository.get_user_authorization( + self.user) + authorization_user.role = RepositoryAuthorization.ROLE_USER + authorization_user.save() + self.assertTrue(authorization_user.can_read) + + # private repository + authorization_user = self.private_repository.get_user_authorization( + self.user) + authorization_user.role = RepositoryAuthorization.ROLE_USER + authorization_user.save() + self.assertTrue(authorization_user.can_read) + + def test_role_user_can_t_contribute(self): + # public repository + authorization_user = self.repository.get_user_authorization( + self.user) + authorization_user.role = RepositoryAuthorization.ROLE_USER + authorization_user.save() + self.assertFalse(authorization_user.can_contribute) + + # private repository + authorization_user = self.private_repository.get_user_authorization( + self.user) + authorization_user.role = RepositoryAuthorization.ROLE_USER + authorization_user.save() + self.assertFalse(authorization_user.can_contribute) + + def test_role_contributor_can_contribute(self): + # public repository + authorization_user = self.repository.get_user_authorization( + self.user) + authorization_user.role = RepositoryAuthorization.ROLE_CONTRIBUTOR + authorization_user.save() + self.assertTrue(authorization_user.can_contribute) + + # private repository + authorization_user = self.private_repository.get_user_authorization( + self.user) + authorization_user.role = RepositoryAuthorization.ROLE_CONTRIBUTOR + authorization_user.save() + self.assertTrue(authorization_user.can_contribute) + class RepositoryUpdateTrainingTestCase(TestCase): def setUp(self):