From 79e968253158e63364c4a592a6f04ba7fff9e89b Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 5 Feb 2015 14:00:21 +0100 Subject: [PATCH 1/3] Updating permissions for supporting public projects --- taiga/base/filters.py | 53 ++++++++++++++----- taiga/projects/permissions.py | 6 +-- taiga/users/api.py | 6 +-- .../test_users_resources.py | 8 +-- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 5b487496..f7ea91bb 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -212,7 +212,7 @@ class CanViewProjectObjFilterBackend(FilterBackend): qs = qs.filter((Q(id__in=projects_list) | Q(public_permissions__contains=["view_project"]))) else: - qs = qs.filter(public_permissions__contains=["view_project"]) + qs = qs.filter(anon_permissions__contains=["view_project"]) return super().filter_queryset(request, qs.distinct(), view) @@ -229,22 +229,49 @@ class IsProjectMemberFilterBackend(FilterBackend): return super().filter_queryset(request, queryset.distinct(), view) -class MembersFilterBackend(filters.BaseFilterBackend): +class MembersFilterBackend(PermissionBasedFilterBackend): + permission = "view_project" + def filter_queryset(self, request, queryset, view): - project_id = request.QUERY_PARAMS.get('project', None) + project_id = None + project = None + qs = queryset + if "project" in request.QUERY_PARAMS: + try: + project_id = int(request.QUERY_PARAMS["project"]) + except: + logger.error("Filtering project diferent value than an integer: {}".format(request.QUERY_PARAMS["project"])) + raise exc.BadRequest("'project' must be an integer value.") + if project_id: - project_model = apps.get_model('projects', 'Project') - project = get_object_or_404(project_model, pk=project_id) - if (request.user.is_authenticated() and - project.memberships.filter(user=request.user).exists()): - return queryset.filter(memberships__project=project).distinct() - else: - raise exc.PermissionDenied(_("You don't have permisions to see this project users.")) + Project = apps.get_model('projects', 'Project') + project = get_object_or_404(Project, pk=project_id) - if request.user.is_superuser: - return queryset + if request.user.is_authenticated() and request.user.is_superuser: + qs = qs + elif request.user.is_authenticated(): + memberships_qs = Membership.objects.filter(user=request.user) + if project_id: + memberships_qs = memberships_qs.filter(project_id=project_id) + memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) | + Q(is_owner=True)) - return [] + projects_list = [membership.project_id for membership in memberships_qs] + + if project and not "view_project" in project.public_permissions: + qs = qs.none() + + qs = qs.filter(Q(memberships__project_id__in=projects_list) | + Q(memberships__project__public_permissions__contains=[self.permission])| + Q(id=request.user.id)) + + else: + if project and not "view_project" in project.anon_permissions: + qs = qs.none() + + qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission]) + + return qs.distinct() class BaseIsProjectAdminFilterBackend(object): diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 5fa9180b..fc9ef228 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -52,12 +52,12 @@ class ProjectPermission(TaigaResourcePermission): destroy_perms = IsProjectOwner() modules_perms = IsProjectOwner() list_perms = AllowAny() - stats_perms = AllowAny() + stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project') star_perms = IsAuthenticated() unstar_perms = IsAuthenticated() - issues_stats_perms = AllowAny() - issues_filters_data_perms = AllowAny() + issues_stats_perms = HasProjectPerm('view_project') + issues_filters_data_perms = HasProjectPerm('view_project') tags_perms = HasProjectPerm('view_project') tags_colors_perms = HasProjectPerm('view_project') fans_perms = HasProjectPerm('view_project') diff --git a/taiga/users/api.py b/taiga/users/api.py index 547e99c6..b582ac20 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -30,6 +30,7 @@ from taiga.auth.tokens import get_user_for_token from taiga.base.decorators import list_route from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet +from taiga.base.filters import PermissionBasedFilterBackend from taiga.base.api.utils import get_object_or_404 from taiga.base.filters import MembersFilterBackend from taiga.projects.votes import services as votes_service @@ -46,14 +47,11 @@ from . import permissions from .signals import user_cancel_account as user_cancel_account_signal -###################################################### -## User -###################################################### - class UsersViewSet(ModelCrudViewSet): permission_classes = (permissions.UserPermission,) serializer_class = serializers.UserSerializer queryset = models.User.objects.all() + filter_backends = (MembersFilterBackend,) def create(self, *args, **kwargs): raise exc.NotSupported() diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py index 0d9bf536..f20eaab4 100644 --- a/tests/integration/resources_permissions/test_users_resources.py +++ b/tests/integration/resources_permissions/test_users_resources.py @@ -86,7 +86,7 @@ def test_user_delete(client, data): ] results = helper_test_http_method(client, 'delete', url, None, users) - assert results == [401, 403, 204] + assert results == [404, 404, 204] def test_user_list(client, data): @@ -101,14 +101,14 @@ def test_user_list(client, data): response = client.get(url) users_data = json.loads(response.content.decode('utf-8')) - assert len(users_data) == 0 + assert len(users_data) == 1 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) == 0 + assert len(users_data) == 1 assert response.status_code == 200 client.login(data.superuser) @@ -146,7 +146,7 @@ def test_user_patch(client, data): patch_data = json.dumps({"full_name": "test"}) results = helper_test_http_method(client, 'patch', url, patch_data, users) - assert results == [401, 200, 403, 200] + assert results == [404, 200, 404, 200] def test_user_action_change_password(client, data): From 44e6f7c42dbb4c7ba20758ad4f094a85bd788054 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 5 Feb 2015 15:07:12 +0100 Subject: [PATCH 2/3] Updating anon and public permissions when project is public --- taiga/base/filters.py | 1 + taiga/permissions/service.py | 9 +++++++++ taiga/projects/api.py | 6 +++++- tests/integration/test_projects.py | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index f7ea91bb..12651e6f 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -250,6 +250,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend): if request.user.is_authenticated() and request.user.is_superuser: qs = qs elif request.user.is_authenticated(): + Membership = apps.get_model('projects', 'Membership') memberships_qs = Membership.objects.filter(user=request.user) if project_id: memberships_qs = memberships_qs.filter(project_id=project_id) diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py index 75376229..3ee86821 100644 --- a/taiga/permissions/service.py +++ b/taiga/permissions/service.py @@ -103,3 +103,12 @@ def get_user_project_permissions(user, project): anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] return set(owner_permissions + members_permissions + public_permissions + anon_permissions) + + +def set_base_permissions_for_public_project(project): + """ + If a project is public anonymous and registered users should have at least visualization permissions + """ + anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) + project.anon_permissions = list(set(project.anon_permissions + anon_permissions)) + project.public_permissions = list(set(project.public_permissions + anon_permissions)) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 0f17c668..3f64ba8e 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -36,6 +36,7 @@ from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin from taiga.projects.userstories.models import UserStory from taiga.projects.tasks.models import Task from taiga.projects.issues.models import Issue +from taiga.permissions import service as permissions_service from . import serializers from . import models @@ -46,7 +47,6 @@ from .votes import serializers as votes_serializers from .votes import services as votes_service from .votes.utils import attach_votescount_to_queryset - ###################################################### ## Project ###################################################### @@ -176,6 +176,10 @@ class ProjectViewSet(ModelCrudViewSet): if not obj.id: obj.template = self.request.QUERY_PARAMS.get('template', None) + # Update anon permissions if the project is public + if obj.is_private == False: + permissions_service.set_base_permissions_for_public_project(obj) + super().pre_save(obj) def destroy(self, request, *args, **kwargs): diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 29a1966f..f7c01b14 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.services import stats as stats_services from taiga.projects.history.services import take_snapshot +from taiga.permissions.permissions import ANON_PERMISSIONS from .. import factories as f @@ -235,3 +236,19 @@ def test_edit_membership_only_owner(client): response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 assert response.data["is_owner"][0] == "At least one of the user must be an active admin" + + +def test_anon_permissions_generation_when_making_project_public(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(is_private=True) + role = f.RoleFactory.create(project=project, permissions=["view_project", "modify_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + assert project.anon_permissions == [] + client.login(user) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + data = {"is_private": False} + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) + assert set(anon_permissions).issubset(set(response.data["anon_permissions"])) + assert set(anon_permissions).issubset(set(response.data["public_permissions"])) From cff56f1d862d2ae022f7a33709f45197b7d05f9d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 9 Mar 2015 11:50:18 +0100 Subject: [PATCH 3/3] Fixing is_private attribute --- taiga/permissions/service.py | 19 +++++++++------ taiga/projects/api.py | 19 +++++++++++---- .../0017_fix_is_private_for_projects.py | 24 +++++++++++++++++++ taiga/projects/models.py | 2 +- 4 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 taiga/projects/migrations/0017_fix_is_private_for_projects.py diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py index 3ee86821..d9df5bd7 100644 --- a/taiga/permissions/service.py +++ b/taiga/permissions/service.py @@ -105,10 +105,15 @@ def get_user_project_permissions(user, project): return set(owner_permissions + members_permissions + public_permissions + anon_permissions) -def set_base_permissions_for_public_project(project): - """ - If a project is public anonymous and registered users should have at least visualization permissions - """ - anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) - project.anon_permissions = list(set(project.anon_permissions + anon_permissions)) - project.public_permissions = list(set(project.public_permissions + anon_permissions)) +def set_base_permissions_for_project(project): + if project.is_private: + project.anon_permissions = [] + project.public_permissions = [] + + else: + """ + If a project is public anonymous and registered users should have at least visualization permissions + """ + anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) + project.anon_permissions = list(set(project.anon_permissions + anon_permissions)) + project.public_permissions = list(set(project.public_permissions + anon_permissions)) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 3f64ba8e..92ce7e28 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -168,6 +168,20 @@ class ProjectViewSet(ModelCrudViewSet): services.remove_user_from_project(request.user, project) return response.Ok() + def _set_base_permissions(self, obj): + update_permissions = False + if not obj.id: + if not obj.is_private: + # Creating a public project + update_permissions = True + else: + if self.get_object().is_private != obj.is_private: + # Changing project public state + update_permissions = True + + if update_permissions: + permissions_service.set_base_permissions_for_project(obj) + def pre_save(self, obj): if not obj.id: obj.owner = self.request.user @@ -176,10 +190,7 @@ class ProjectViewSet(ModelCrudViewSet): if not obj.id: obj.template = self.request.QUERY_PARAMS.get('template', None) - # Update anon permissions if the project is public - if obj.is_private == False: - permissions_service.set_base_permissions_for_public_project(obj) - + self._set_base_permissions(obj) super().pre_save(obj) def destroy(self, request, *args, **kwargs): diff --git a/taiga/projects/migrations/0017_fix_is_private_for_projects.py b/taiga/projects/migrations/0017_fix_is_private_for_projects.py new file mode 100644 index 00000000..c94e6a63 --- /dev/null +++ b/taiga/projects/migrations/0017_fix_is_private_for_projects.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def update_existing_projects(apps, schema_editor): + Project = apps.get_model("projects", "Project") + Project.objects.filter(is_private=False).update(is_private=True) + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0016_fix_json_field_not_null'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='is_private', + field=models.BooleanField(verbose_name='is private', default=True), + preserve_default=True, + ), + migrations.RunPython(update_existing_projects), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 7912ceab..05551d92 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -160,7 +160,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): default=[], verbose_name=_("user permissions"), choices=USER_PERMISSIONS) - is_private = models.BooleanField(default=False, null=False, blank=True, + is_private = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("is private")) tags_colors = TextArrayField(dimension=2, null=False, blank=True, verbose_name=_("tags colors"), default=[])