Merge branch 'feature/15' into 'develop'
Fixed #15: Ensure we check for authorization for serving audio files, meaning we… Closes #15 See merge request !12
This commit is contained in:
commit
795cd7beb9
|
@ -1,2 +0,0 @@
|
||||||
FROM nginx:latest
|
|
||||||
ADD nginx.conf /etc/nginx/nginx.conf
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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'
|
|
@ -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)
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
20
dev.yml
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
-------
|
-------
|
||||||
|
|
Loading…
Reference in New Issue