Fixed #15: Ensure we check for authorization for serving audio files, meaning we don't leak the absolute URL anymore

merge-requests/154/head
Eliot Berriot 2017-06-28 23:30:26 +02:00
rodzic e250759880
commit bab3981d25
11 zmienionych plików z 147 dodań i 30 usunięć

Wyświetl plik

@ -1,2 +0,0 @@
FROM nginx:latest
ADD nginx.conf /etc/nginx/nginx.conf

Wyświetl plik

@ -8,6 +8,7 @@ from rest_framework_jwt import views as jwt_views
router = routers.SimpleRouter() router = routers.SimpleRouter()
router.register(r'tags', views.TagViewSet, 'tags') router.register(r'tags', views.TagViewSet, 'tags')
router.register(r'tracks', views.TrackViewSet, 'tracks') router.register(r'tracks', views.TrackViewSet, 'tracks')
router.register(r'trackfiles', views.TrackFileViewSet, 'trackfiles')
router.register(r'artists', views.ArtistViewSet, 'artists') router.register(r'artists', views.ArtistViewSet, 'artists')
router.register(r'albums', views.AlbumViewSet, 'albums') router.register(r'albums', views.AlbumViewSet, 'albums')
router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches') router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches')

Wyświetl plik

@ -217,7 +217,6 @@ STATICFILES_FINDERS = (
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR('media')) MEDIA_ROOT = str(APPS_DIR('media'))
USE_SAMPLE_TRACK = env.bool("USE_SAMPLE_TRACK", False)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
@ -261,7 +260,6 @@ BROKER_URL = env("CELERY_BROKER_URL", default='django://')
# Location of root django.contrib.admin URL, use {% url 'admin:index' %} # Location of root django.contrib.admin URL, use {% url 'admin:index' %}
ADMIN_URL = r'^admin/' ADMIN_URL = r'^admin/'
SESSION_SAVE_EVERY_REQUEST = True
# Your common stuff: Below this line define 3rd party library settings # Your common stuff: Below this line define 3rd party library settings
CELERY_DEFAULT_RATE_LIMIT = 1 CELERY_DEFAULT_RATE_LIMIT = 1
CELERYD_TASK_TIME_LIMIT = 300 CELERYD_TASK_TIME_LIMIT = 300
@ -305,3 +303,13 @@ FUNKWHALE_PROVIDERS = {
} }
} }
ATOMIC_REQUESTS = False ATOMIC_REQUESTS = False
# Wether we should check user permission before serving audio files (meaning
# return an obfuscated url)
# This require a special configuration on the reverse proxy side
# See https://wellfire.co/learn/nginx-django-x-accel-redirects/ for example
PROTECT_AUDIO_FILES = env.bool('PROTECT_AUDIO_FILES', default=True)
# Which path will be used to process the internal redirection
# **DO NOT** put a slash at the end
PROTECT_FILES_PATH = env('PROTECT_FILES_PATH', default='/_protected')

Wyświetl plik

@ -8,7 +8,6 @@ import markdown
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files import File from django.core.files import File
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -354,10 +353,12 @@ class TrackFile(models.Model):
@property @property
def path(self): def path(self):
if settings.USE_SAMPLE_TRACK: if settings.PROTECT_AUDIO_FILES:
return static('music/sample1.ogg') return reverse(
'api:v1:trackfiles-serve', kwargs={'pk': self.pk})
return self.audio_file.url return self.audio_file.url
class ImportBatch(models.Model): class ImportBatch(models.Model):
creation_date = models.DateTimeField(default=timezone.now) creation_date = models.DateTimeField(default=timezone.now)
submitted_by = models.ForeignKey('users.User', related_name='imports') submitted_by = models.ForeignKey('users.User', related_name='imports')

Wyświetl plik

@ -0,0 +1,39 @@
import factory
class ArtistFactory(factory.django.DjangoModelFactory):
name = factory.Sequence(lambda n: 'artist-{0}'.format(n))
mbid = factory.Faker('uuid4')
class Meta:
model = 'music.Artist'
class AlbumFactory(factory.django.DjangoModelFactory):
title = factory.Sequence(lambda n: 'album-{0}'.format(n))
mbid = factory.Faker('uuid4')
release_date = factory.Faker('date')
cover = factory.django.ImageField()
artist = factory.SubFactory(ArtistFactory)
class Meta:
model = 'music.Album'
class TrackFactory(factory.django.DjangoModelFactory):
title = factory.Sequence(lambda n: 'track-{0}'.format(n))
mbid = factory.Faker('uuid4')
album = factory.SubFactory(AlbumFactory)
artist = factory.SelfAttribute('album.artist')
position = 1
class Meta:
model = 'music.Track'
class TrackFileFactory(factory.django.DjangoModelFactory):
track = factory.SubFactory(TrackFactory)
audio_file = factory.django.FileField()
class Meta:
model = 'music.TrackFile'

Wyświetl plik

@ -10,6 +10,8 @@ from funkwhale_api.music import serializers
from funkwhale_api.users.models import User from funkwhale_api.users.models import User
from . import data as api_data from . import data as api_data
from . import factories
class TestAPI(TMPDirTestCaseMixin, TestCase): class TestAPI(TMPDirTestCaseMixin, TestCase):
@ -214,3 +216,26 @@ class TestAPI(TMPDirTestCaseMixin, TestCase):
with self.settings(API_AUTHENTICATION_REQUIRED=False): with self.settings(API_AUTHENTICATION_REQUIRED=False):
response = getattr(self.client, method)(url) response = getattr(self.client, method)(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_track_file_url_is_restricted_to_authenticated_users(self):
f = factories.TrackFileFactory()
self.assertNotEqual(f.audio_file, None)
url = f.path
with self.settings(API_AUTHENTICATION_REQUIRED=True):
response = self.client.get(url)
self.assertEqual(response.status_code, 401)
user = User.objects.create_superuser(
username='test', email='test@test.com', password='test')
self.client.login(username=user.username, password='test')
with self.settings(API_AUTHENTICATION_REQUIRED=True):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response['X-Accel-Redirect'],
'/_protected{}'.format(f.audio_file.url)
)

Wyświetl plik

@ -3,6 +3,7 @@ import json
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models, transaction from django.db import models, transaction
from django.db.models.functions import Length from django.db.models.functions import Length
from django.conf import settings
from rest_framework import viewsets, views from rest_framework import viewsets, views
from rest_framework.decorators import detail_route, list_route from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
@ -51,6 +52,7 @@ class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
search_fields = ['name'] search_fields = ['name']
ordering_fields = ('creation_date',) ordering_fields = ('creation_date',)
class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
queryset = ( queryset = (
models.Album.objects.all() models.Album.objects.all()
@ -63,6 +65,7 @@ class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet):
search_fields = ['title'] search_fields = ['title']
ordering_fields = ('creation_date',) ordering_fields = ('creation_date',)
class ImportBatchViewSet(viewsets.ReadOnlyModelViewSet): class ImportBatchViewSet(viewsets.ReadOnlyModelViewSet):
queryset = models.ImportBatch.objects.all().order_by('-creation_date') queryset = models.ImportBatch.objects.all().order_by('-creation_date')
serializer_class = serializers.ImportBatchSerializer serializer_class = serializers.ImportBatchSerializer
@ -70,6 +73,7 @@ class ImportBatchViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter(submitted_by=self.request.user) return super().get_queryset().filter(submitted_by=self.request.user)
class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet): class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
""" """
A simple ViewSet for viewing and editing accounts. A simple ViewSet for viewing and editing accounts.
@ -120,6 +124,27 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
return Response(serializer.data) return Response(serializer.data)
class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
queryset = (models.TrackFile.objects.all().order_by('-id'))
serializer_class = serializers.TrackFileSerializer
permission_classes = [ConditionalAuthentication]
@detail_route(methods=['get'])
def serve(self, request, *args, **kwargs):
try:
f = models.TrackFile.objects.get(pk=kwargs['pk'])
except models.TrackFile.DoesNotExist:
return Response(status=404)
response = Response()
response["Content-Disposition"] = "attachment; filename={0}".format(
f.audio_file.name)
response['X-Accel-Redirect'] = "{}{}".format(
settings.PROTECT_FILES_PATH,
f.audio_file.url)
return response
class TagViewSet(viewsets.ReadOnlyModelViewSet): class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all().order_by('name') queryset = Tag.objects.all().order_by('name')
serializer_class = serializers.TagSerializer serializer_class = serializers.TagSerializer

Wyświetl plik

@ -47,7 +47,17 @@ server {
location /media/ { location /media/ {
alias /srv/funkwhale/data/media/; alias /srv/funkwhale/data/media/;
} }
location /_protected/media {
# this is an internal location that is used to serve
# audio files once correct permission / authentication
# has been checked on API side
internal;
alias /srv/funkwhale/data/media;
}
location /staticfiles/ { location /staticfiles/ {
# django static files
alias /srv/funkwhale/data/static/; alias /srv/funkwhale/data/static/;
} }
} }

20
dev.yml
Wyświetl plik

@ -53,12 +53,14 @@ services:
- redis - redis
- celeryworker - celeryworker
# nginx: nginx:
# env_file: .env.dev env_file: .env.dev
# build: ./api/compose/nginx image: nginx
# links: links:
# - api - api
# volumes: - front
# - ./api/funkwhale_api/media:/staticfiles/media volumes:
# ports: - ./docker/nginx/conf.dev:/etc/nginx/nginx.conf
# - "0.0.0.0:6001:80" - ./api/funkwhale_api/media:/protected/media
ports:
- "0.0.0.0:6001:80"

Wyświetl plik

@ -27,27 +27,21 @@ http {
#gzip on; #gzip on;
upstream app {
server django:12081;
}
server { server {
listen 80; listen 80;
charset utf-8; charset utf-8;
root /staticfiles; location /_protected/media {
internal;
alias /protected/media;
}
location / { location / {
# checks for static file, if not found proxy to app proxy_set_header Host $host;
try_files $uri @proxy_to_app; proxy_set_header X-Real-IP $remote_addr;
}
location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off; proxy_redirect off;
proxy_pass http://api:12081/;
proxy_pass http://app;
} }
} }
} }

Wyświetl plik

@ -1,6 +1,20 @@
Changelog Changelog
========= =========
next
-------
* [breaking] we now check for user permission before serving audio files, which requires
a specific configuration block in your reverse proxy configuration:
.. code-block::
location /_protected/media {
internal;
alias /srv/funkwhale/data/media;
}
0.1 0.1
------- -------