API for getting signed files (bug 1221759)

This commit is contained in:
Kumar McMillan 2015-11-04 17:31:47 -06:00
Родитель eb29c1d610
Коммит c32b93c418
10 изменённых файлов: 157 добавлений и 25 удалений

Просмотреть файл

@ -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.