diff --git a/settings/common.py b/settings/common.py index 2caacaa1..c50ad278 100644 --- a/settings/common.py +++ b/settings/common.py @@ -76,7 +76,7 @@ SITES = { SITE_ID = "api" # Session configuration (only used for admin) -SESSION_ENGINE="django.contrib.sessions.backends.db" +SESSION_ENGINE = "django.contrib.sessions.backends.db" SESSION_COOKIE_AGE = 1209600 # (2 weeks) # MAIL OPTIONS @@ -325,3 +325,8 @@ GRAVATAR_DEFAULT_OPTIONS = { 'default': DEFAULT_AVATAR_URL, # default avatar to show if there's no gravatar image 'size': DEFAULT_AVATAR_SIZE } + +try: + IN_DEVELOPMENT_SERVER = sys.argv[1] == 'runserver' +except IndexError: + IN_DEVELOPMENT_SERVER = False diff --git a/settings/testing.py b/settings/testing.py index 4448be54..13fe1999 100644 --- a/settings/testing.py +++ b/settings/testing.py @@ -20,3 +20,5 @@ SKIP_SOUTH_TESTS = True SOUTH_TESTS_MIGRATE = False CELERY_ALWAYS_EAGER = True + +MEDIA_ROOT = "/tmp" diff --git a/taiga/base/utils/urls.py b/taiga/base/utils/urls.py index 2e5d71be..0b94b5aa 100644 --- a/taiga/base/utils/urls.py +++ b/taiga/base/utils/urls.py @@ -1,4 +1,5 @@ import django_sites as sites +from django.core.urlresolvers import reverse as django_reverse URL_TEMPLATE = "{scheme}://{domain}/{path}" @@ -18,3 +19,8 @@ def get_absolute_url(path): return path site = sites.get_current() return build_url(path, scheme=site.scheme, domain=site.domain) + + +def reverse(viewname, *args, **kwargs): + """Same behavior as django's reverse but uses django_sites to compute absolute url.""" + return get_absolute_url(django_reverse(viewname, *args, **kwargs)) diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index eb74043d..92ecdb40 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -13,13 +13,13 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from os import path from rest_framework import serializers +from taiga.base.utils.urls import reverse from . import models -from os import path - class AttachmentSerializer(serializers.ModelSerializer): name = serializers.SerializerMethodField("get_name") @@ -39,7 +39,7 @@ class AttachmentSerializer(serializers.ModelSerializer): return "" def get_url(self, obj): - return obj.attached_file.url if obj and obj.attached_file else "" + return reverse("attachment-url", kwargs={"pk": obj.pk}) def get_size(self, obj): if obj.attached_file: diff --git a/taiga/projects/attachments/views.py b/taiga/projects/attachments/views.py new file mode 100644 index 00000000..567944ba --- /dev/null +++ b/taiga/projects/attachments/views.py @@ -0,0 +1,32 @@ +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/urls.py b/taiga/urls.py index b8ffe02f..74daefd7 100644 --- a/taiga/urls.py +++ b/taiga/urls.py @@ -20,11 +20,14 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin from .routers import router +from .projects.attachments.views import RawAttachmentView + admin.autodiscover() urlpatterns = patterns('', + url(r'^attachments/(?P\d+)/$', RawAttachmentView.as_view(), name="attachment-url"), url(r'^api/v1/', include(router.urls)), url(r'^api/v1/api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^admin/', include(admin.site.urls)), diff --git a/tests/factories.py b/tests/factories.py index c80225fd..cd1d1361 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -258,6 +258,16 @@ class ContentTypeFactory(Factory): model = factory.LazyAttribute(lambda obj: ContentTypeFactory.FACTORY_FOR._meta.model_name) +class AttachmentFactory(Factory): + FACTORY_FOR = get_model("attachments", "Attachment") + + owner = factory.SubFactory("tests.factories.UserFactory") + project = factory.SubFactory("tests.factories.ProjectFactory") + content_type = factory.SubFactory("tests.factories.ContentTypeFactory") + object_id = factory.Sequence(lambda n: n) + attached_file = factory.django.FileField(data=b"File contents") + + def create_issue(**kwargs): "Create an issue and along with its dependencies." owner = kwargs.pop("owner", UserFactory()) diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py new file mode 100644 index 00000000..77ffcf69 --- /dev/null +++ b/tests/integration/test_attachments.py @@ -0,0 +1,58 @@ +import pytest + +from django.core.urlresolvers import reverse +from django.core.files.base import File + +from .. import factories as f +from ..utils import set_settings + +pytestmark = pytest.mark.django_db + +def test_authentication(client): + "User can't access an attachment if not authenticated" + attachment = f.AttachmentFactory.create() + url = reverse("attachment-url", kwargs={"pk": attachment.pk}) + + response = client.get(url) + + assert response.status_code == 401 + + +def test_authorization(client): + "User can't access an attachment if not authorized" + attachment = f.AttachmentFactory.create() + user = f.UserFactory.create() + + url = reverse("attachment-url", kwargs={"pk": attachment.pk}) + + client.login(user) + response = client.get(url) + + assert response.status_code == 403 + + +@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() + + url = reverse("attachment-url", kwargs={"pk": attachment.pk}) + + client.login(attachment.owner) + response = client.get(url) + + assert response.status_code == 302 + + +@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() + + url = reverse("attachment-url", kwargs={"pk": attachment.pk}) + + client.login(attachment.owner) + response = client.get(url) + + assert response.status_code == 200 + assert response.has_header('x-accel-redirect')