add /media generic view (#75378)
gitea/passerelle/pipeline/head This commit looks good Details

This commit is contained in:
Thomas NOËL 2023-03-14 16:23:17 +01:00 committed by Gitea
parent 019559f0c1
commit 88f787afe2
3 changed files with 80 additions and 9 deletions

View File

@ -1,9 +1,7 @@
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.views.static import serve as static_serve
from .api.urls import urlpatterns as api_urls from .api.urls import urlpatterns as api_urls
from .base.urls import access_urlpatterns, import_export_urlpatterns from .base.urls import access_urlpatterns, import_export_urlpatterns
@ -22,6 +20,7 @@ from .views import (
HomePageView, HomePageView,
ManageAddView, ManageAddView,
ManageView, ManageView,
MediaView,
login, login,
logout, logout,
menu_json, menu_json,
@ -34,13 +33,7 @@ urlpatterns = [
path('manage/', manager_required(ManageView.as_view()), name='manage-home'), path('manage/', manager_required(ManageView.as_view()), name='manage-home'),
re_path(r'^manage/menu.json$', manager_required(menu_json), name='menu-json'), re_path(r'^manage/menu.json$', manager_required(menu_json), name='menu-json'),
path('manage/add', manager_required(ManageAddView.as_view()), name='add-connector'), path('manage/add', manager_required(ManageAddView.as_view()), name='add-connector'),
re_path( re_path(r'^media/(?P<path>.*)$', manager_required(MediaView.as_view()), name='media'),
r'^media/(?P<path>.*)$',
login_required(static_serve),
{
'document_root': settings.MEDIA_ROOT,
},
),
re_path(r'^admin/', admin.site.urls), re_path(r'^admin/', admin.site.urls),
re_path(r'^manage/access/', decorated_includes(manager_required, include(access_urlpatterns))), re_path(r'^manage/access/', decorated_includes(manager_required, include(access_urlpatterns))),
re_path(r'^manage/', decorated_includes(manager_required, include(import_export_urlpatterns))), re_path(r'^manage/', decorated_includes(manager_required, include(import_export_urlpatterns))),

View File

@ -19,6 +19,7 @@ import hashlib
import inspect import inspect
import json import json
import logging import logging
import os
import uuid import uuid
from urllib.parse import quote from urllib.parse import quote
@ -29,6 +30,7 @@ from django.contrib.auth import logout as auth_logout
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.files.storage import DefaultStorage
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
@ -50,6 +52,7 @@ from django.views.generic import (
View, View,
) )
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.static import serve
from jsonschema import ValidationError, validate, validators from jsonschema import ValidationError, validate, validators
from passerelle.base.models import BaseResource, ResourceLog from passerelle.base.models import BaseResource, ResourceLog
@ -629,3 +632,18 @@ class GenericExportConnectorView(GenericConnectorMixin, DetailView):
) )
json.dump({'resources': [self.get_object().export_json()]}, response, indent=2) json.dump({'resources': [self.get_object().export_json()]}, response, indent=2)
return response return response
class MediaView(View):
def get(self, request, path, *args, **kwargs):
document_root = DefaultStorage().location
filename = DefaultStorage().path(path)
filename = os.path.realpath(filename)
if (
not os.path.isabs(filename)
or not filename.startswith(document_root)
or not os.path.exists(filename)
or not os.path.isfile(filename)
):
raise Http404()
return serve(request, path, document_root=document_root, show_indexes=False)

60
tests/test_media.py Normal file
View File

@ -0,0 +1,60 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2023 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
from django.core.files.base import ContentFile
from passerelle.apps.pdf.models import Resource
from tests.test_manager import login
from tests.utils import setup_access_rights
@pytest.fixture
def pdf(db):
return setup_access_rights(Resource.objects.create(slug='test', title='test', description='test'))
@pytest.fixture
def cerfa_content():
with open('tests/data/cerfa_10072-02.pdf', 'rb') as fd:
return fd.read()
def test_media(app, admin_user, simple_user, pdf, cerfa_content):
pdf.fill_form_file.save('form.pdf', ContentFile(cerfa_content))
# refuse anonymous or simple user
resp = app.get('/media/pdf/test/form.pdf', status=302)
assert resp.location == '/login/?next=/media/pdf/test/form.pdf'
app = login(app, username='user', password='user')
resp = app.get('/media/pdf/test/form.pdf', status=403)
# allow manager access
app = login(app, username='admin', password='admin')
resp = app.get('/media/pdf/test/form.pdf')
assert resp.content.startswith(b'%PDF')
assert resp.headers['content-type'] == 'application/pdf'
# bad requests: 404 or 400
resp = app.get('/media/pdf/plop/there-is-not-file-here.pdf', status=404)
resp = app.get('/media/pdf/bad-slug/form.pdf', status=404)
resp = app.get('/media/pdf/', status=404)
resp = app.get('/media/pdf', status=404)
resp = app.get('/media/', status=404)
resp = app.get('/media/../etc/passwd', status=400)
resp = app.get('/media/../../../../../../../../etc/passwd', status=400)