API for getting signed files (bug 1221759)
This commit is contained in:
Родитель
eb29c1d610
Коммит
c32b93c418
|
@ -98,7 +98,7 @@ def check_addon_ownership(request, addon, viewer=False, dev=False,
|
|||
# Support can do support.
|
||||
elif support:
|
||||
roles += (amo.AUTHOR_ROLE_SUPPORT,)
|
||||
return addon.authors.filter(pk=request.amo_user.pk,
|
||||
return addon.authors.filter(pk=request.user.pk,
|
||||
addonuser__role__in=roles).exists()
|
||||
|
||||
|
||||
|
|
|
@ -89,10 +89,14 @@ class TestHasPerm(TestCase):
|
|||
self.addon = Addon.objects.get(id=3615)
|
||||
self.au = AddonUser.objects.get(addon=self.addon, user=self.user)
|
||||
assert self.au.role == amo.AUTHOR_ROLE_OWNER
|
||||
self.request = mock.Mock()
|
||||
self.request.groups = ()
|
||||
self.request.amo_user = self.user
|
||||
self.request.user.is_authenticated.return_value = True
|
||||
self.request = self.fake_request_with_user(self.user)
|
||||
|
||||
def fake_request_with_user(self, user):
|
||||
request = mock.Mock()
|
||||
request.groups = user.groups.all()
|
||||
request.user = user
|
||||
request.user.is_authenticated = mock.Mock(return_value=True)
|
||||
return request
|
||||
|
||||
def login_admin(self):
|
||||
assert self.client.login(username='admin@mozilla.com',
|
||||
|
@ -105,8 +109,7 @@ class TestHasPerm(TestCase):
|
|||
assert not check_addon_ownership(self.request, self.addon)
|
||||
|
||||
def test_admin(self):
|
||||
self.request.amo_user = self.login_admin()
|
||||
self.request.groups = self.request.amo_user.groups.all()
|
||||
self.request = self.fake_request_with_user(self.login_admin())
|
||||
assert check_addon_ownership(self.request, self.addon)
|
||||
assert check_addon_ownership(self.request, self.addon, admin=True)
|
||||
assert not check_addon_ownership(self.request, self.addon, admin=False)
|
||||
|
@ -115,8 +118,8 @@ class TestHasPerm(TestCase):
|
|||
assert check_ownership(self.request, self.addon, require_author=True)
|
||||
|
||||
def test_require_author_when_admin(self):
|
||||
self.request.amo_user = self.login_admin()
|
||||
self.request.groups = self.request.amo_user.groups.all()
|
||||
self.request = self.fake_request_with_user(self.login_admin())
|
||||
self.request.groups = self.request.user.groups.all()
|
||||
assert check_ownership(self.request, self.addon, require_author=False)
|
||||
|
||||
assert not check_ownership(self.request, self.addon,
|
||||
|
|
|
@ -131,10 +131,15 @@ class File(amo.models.OnChangeMixin, amo.models.ModelBase):
|
|||
return posixpath.join(*map(smart_str, [host, addon.id, self.filename]))
|
||||
|
||||
def get_url_path(self, src):
|
||||
return self._make_download_url('downloads.file', src)
|
||||
|
||||
def get_signed_url(self, src):
|
||||
return self._make_download_url('signing.file', src)
|
||||
|
||||
def _make_download_url(self, view_name, src):
|
||||
from amo.helpers import urlparams, absolutify
|
||||
url = os.path.join(reverse('downloads.file', args=[self.id]),
|
||||
url = os.path.join(reverse(view_name, args=[self.pk]),
|
||||
self.filename)
|
||||
# Firefox's Add-on Manager needs absolute urls.
|
||||
return absolutify(urlparams(url, src=src))
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -68,6 +68,18 @@ class TestFile(amo.tests.TestCase, amo.tests.AMOPaths):
|
|||
'delicious_bookmarks-2.1.072-fx.xpi?src=src')
|
||||
assert url.endswith(expected), url
|
||||
|
||||
def test_get_url_path(self):
|
||||
file_ = File.objects.get(id=67442)
|
||||
assert file_.get_url_path('src') == \
|
||||
file_.get_absolute_url(src='src')
|
||||
|
||||
def test_get_signed_url(self):
|
||||
file_ = File.objects.get(id=67442)
|
||||
url = file_.get_signed_url('src')
|
||||
expected = ('/api/v3/file/67442/'
|
||||
'delicious_bookmarks-2.1.072-fx.xpi?src=src')
|
||||
assert url.endswith(expected), url
|
||||
|
||||
def check_delete(self, file_, filename):
|
||||
"""Test that when the File object is deleted, it is removed from the
|
||||
filesystem."""
|
||||
|
|
|
@ -50,7 +50,7 @@ class FileUploadSerializer(serializers.ModelSerializer):
|
|||
|
||||
def get_files(self, instance):
|
||||
if self.version is not None:
|
||||
return [{'download_url': f.get_url_path('api'),
|
||||
return [{'download_url': f.get_signed_url('api'),
|
||||
'signed': f.is_signed}
|
||||
for f in self.version.files.all()]
|
||||
else:
|
||||
|
|
|
@ -4,23 +4,38 @@ from datetime import datetime
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
import mock
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
import amo
|
||||
from addons.models import Addon
|
||||
from addons.models import Addon, AddonUser
|
||||
from api.tests.test_jwt_auth import JWTAuthTester
|
||||
from files.models import FileUpload
|
||||
from files.models import File, FileUpload
|
||||
from signing.views import VersionView
|
||||
from users.models import UserProfile
|
||||
from versions.models import Version
|
||||
|
||||
|
||||
class BaseUploadVersionCase(APITestCase, JWTAuthTester):
|
||||
class SigningAPITestCase(APITestCase, JWTAuthTester):
|
||||
fixtures = ['base/addon_3615']
|
||||
|
||||
def setUp(self):
|
||||
self.user = UserProfile.objects.get(email='del@icio.us')
|
||||
self.api_key = self.create_api_key(self.user, 'foo')
|
||||
|
||||
def authorization(self):
|
||||
token = self.create_auth_token(self.api_key.user, self.api_key.key,
|
||||
self.api_key.secret)
|
||||
return 'JWT {}'.format(token)
|
||||
|
||||
def get(self, url):
|
||||
return self.client.get(url, HTTP_AUTHORIZATION=self.authorization())
|
||||
|
||||
|
||||
class BaseUploadVersionCase(SigningAPITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseUploadVersionCase, self).setUp()
|
||||
self.guid = '{2fa4ed95-0317-4c6a-a74c-5f3e3912c1f9}'
|
||||
self.view = VersionView.as_view()
|
||||
|
||||
|
@ -30,11 +45,6 @@ class BaseUploadVersionCase(APITestCase, JWTAuthTester):
|
|||
def create_version(self, version):
|
||||
self.put(self.url(self.guid, version), version)
|
||||
|
||||
def authorization(self):
|
||||
token = self.create_auth_token(self.api_key.user, self.api_key.key,
|
||||
self.api_key.secret)
|
||||
return 'JWT {}'.format(token)
|
||||
|
||||
def xpi_filepath(self, addon, version):
|
||||
return os.path.join(
|
||||
'apps', 'signing', 'fixtures',
|
||||
|
@ -48,9 +58,6 @@ class BaseUploadVersionCase(APITestCase, JWTAuthTester):
|
|||
return self.client.put(url, {'upload': upload},
|
||||
HTTP_AUTHORIZATION=self.authorization())
|
||||
|
||||
def get(self, url):
|
||||
return self.client.get(url, HTTP_AUTHORIZATION=self.authorization())
|
||||
|
||||
|
||||
class TestUploadVersion(BaseUploadVersionCase):
|
||||
|
||||
|
@ -150,6 +157,18 @@ class TestCheckVersion(BaseUploadVersionCase):
|
|||
assert response.status_code == 200
|
||||
assert 'processed' in response.data
|
||||
|
||||
def test_version_download_url(self):
|
||||
version_string = '3.0'
|
||||
qs = File.objects.filter(version__addon__guid=self.guid,
|
||||
version__version=version_string)
|
||||
assert not qs.exists()
|
||||
self.create_version(version_string)
|
||||
response = self.get(self.url(self.guid, version_string))
|
||||
assert response.status_code == 200
|
||||
file_ = qs.get()
|
||||
assert response.data['files'][0]['download_url'] == \
|
||||
file_.get_signed_url('api')
|
||||
|
||||
def test_has_failed_upload(self):
|
||||
addon = Addon.objects.get(guid=self.guid)
|
||||
FileUpload.objects.create(addon=addon, version='3.0')
|
||||
|
@ -157,3 +176,37 @@ class TestCheckVersion(BaseUploadVersionCase):
|
|||
response = self.get(self.url(self.guid, '3.0'))
|
||||
assert response.status_code == 200
|
||||
assert 'processed' in response.data
|
||||
|
||||
|
||||
class TestSignedFile(SigningAPITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSignedFile, self).setUp()
|
||||
self.file_ = self.create_file()
|
||||
|
||||
def url(self):
|
||||
return reverse('signing.file', args=[self.file_.pk])
|
||||
|
||||
def create_file(self):
|
||||
addon = Addon.objects.create(name='thing', is_listed=False)
|
||||
addon.save()
|
||||
AddonUser.objects.create(user=self.user, addon=addon)
|
||||
version = Version.objects.create(addon=addon)
|
||||
return File.objects.create(version=version)
|
||||
|
||||
def test_can_download_once_authenticated(self):
|
||||
response = self.get(self.url())
|
||||
assert response.status_code == 302
|
||||
assert response['X-Target-Digest'] == self.file_.hash
|
||||
|
||||
def test_cannot_download_without_authentication(self):
|
||||
response = self.client.get(self.url()) # no auth
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_api_relies_on_version_downloader(self):
|
||||
with mock.patch('versions.views.download_file') as df:
|
||||
df.return_value = Response({})
|
||||
self.get(self.url())
|
||||
assert df.called is True
|
||||
assert df.call_args[0][0].user == self.user
|
||||
assert df.call_args[0][1] == str(self.file_.pk)
|
||||
|
|
|
@ -6,4 +6,8 @@ urlpatterns = [
|
|||
url(r'^addons/(?P<guid>[^/]+)/versions/(?P<version_string>[^/]+)/$',
|
||||
views.VersionView.as_view(),
|
||||
name='signing.version'),
|
||||
# .* at the end to match filenames.
|
||||
# /file/:id/some-file.xpi
|
||||
url('^file/(?P<file_id>\d+)(?:/.*)?',
|
||||
views.SignedFile.as_view(), name='signing.file'),
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@ from api.jwt_auth.views import JWTProtectedView
|
|||
from devhub.views import handle_upload
|
||||
from files.models import FileUpload
|
||||
from files.utils import parse_addon
|
||||
from versions import views as version_views
|
||||
from versions.models import Version
|
||||
from signing.serializers import FileUploadSerializer
|
||||
|
||||
|
@ -97,3 +98,9 @@ class VersionView(JWTProtectedView):
|
|||
|
||||
serializer = FileUploadSerializer(file_upload, version=version)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class SignedFile(JWTProtectedView):
|
||||
|
||||
def get(self, request, file_id):
|
||||
return version_views.download_file(request, file_id)
|
||||
|
|
|
@ -93,9 +93,15 @@ def download_file(request, file_id, type=None):
|
|||
acl.check_addons_reviewer(request)):
|
||||
return HttpResponseSendFile(request, file.guarded_file_path,
|
||||
content_type='application/x-xpinstall')
|
||||
log.info(u'download file {file_id}: addon/file disabled or user '
|
||||
u'{user_id} is not an owner'.format(file_id=file_id,
|
||||
user_id=request.user.pk))
|
||||
raise http.Http404()
|
||||
|
||||
if not (addon.is_listed or owner_or_unlisted_reviewer(request, addon)):
|
||||
log.info(u'download file {file_id}: addon is unlisted but user '
|
||||
u'{user_id} is not an owner'.format(file_id=file_id,
|
||||
user_id=request.user.pk))
|
||||
raise http.Http404 # Not listed, not owner or admin.
|
||||
|
||||
attachment = (type == 'attachment' or not request.APP.browser)
|
||||
|
|
|
@ -93,7 +93,7 @@ property.
|
|||
"active": true,
|
||||
"files": [
|
||||
{
|
||||
"download_url": "https://addons.mozilla.org/firefox/downloads/file/100/unlisted_wat-1.0-fx+an.xpi?src=api",
|
||||
"download_url": "https://addons.mozilla.org/api/v3/downloads/file/100/unlisted_wat-1.0-fx+an.xpi?src=api",
|
||||
"signed": true
|
||||
}
|
||||
],
|
||||
|
@ -108,7 +108,8 @@ property.
|
|||
}
|
||||
|
||||
:>json active: version is active.
|
||||
:>json files.download_url: URL to download the add-on file.
|
||||
:>json files.download_url:
|
||||
URL to :ref:`download the add-on file <download-signed-file>`.
|
||||
:>json files.signed: if the file is signed.
|
||||
:>json passed_review: if the version has passed review.
|
||||
:>json processed: if the version has been processed by the validator.
|
||||
|
@ -123,3 +124,44 @@ property.
|
|||
:statuscode 401: authentication failed.
|
||||
:statuscode 403: you do not own this add-on.
|
||||
:statuscode 404: add-on or version not found.
|
||||
|
||||
.. _download-signed-file:
|
||||
|
||||
------------------------
|
||||
Downloading signed files
|
||||
------------------------
|
||||
|
||||
When checking on your :ref:`request to sign a version <version-status>`,
|
||||
a successful response will give you an API URL to download the signed files.
|
||||
This endpoint returns the actual file data for download.
|
||||
|
||||
.. http:get:: /api/v3/file/[int:file_id]/[string:base_filename]
|
||||
|
||||
**Request:**
|
||||
|
||||
.. sourcecode:: bash
|
||||
|
||||
curl 'https://addons.mozilla.org/api/v3/file/123/some-addon.xpi?src=api'
|
||||
-H 'Authorization: JWT <jwt-token>'
|
||||
|
||||
:param file_id: the primary key of the add-on file.
|
||||
:param base_filename:
|
||||
the base filename. This is just a convenience for
|
||||
clients so that they write meaningful file names to disk.
|
||||
|
||||
**Response:**
|
||||
|
||||
There are two possible responses:
|
||||
|
||||
* Binary data containing the file
|
||||
* A header that redirects you to a mirror URL for the file.
|
||||
In this case, the initial response will include a
|
||||
``SHA-256`` hash of the file in the header ``X-Target-Digest``.
|
||||
Clients should check that the final downloaded file matches
|
||||
this hash.
|
||||
|
||||
:statuscode 200: request successful.
|
||||
:statuscode 302: file resides at a mirror URL
|
||||
:statuscode 401: authentication failed.
|
||||
:statuscode 404: file does not exist or requester does not have
|
||||
access to it.
|
||||
|
|
Загрузка…
Ссылка в новой задаче