Skip to content

Commit 970e277

Browse files
committed
Add Enterprise Add-on API
1 parent 846a54b commit 970e277

8 files changed

Lines changed: 178 additions & 15 deletions

File tree

docs/topics/api/addons.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,9 @@ This endpoint allows you to list all versions belonging to a specific add-on.
497497
a user account listed as a developer of the add-on.
498498
all_with_deleted Show all versions attached to this add-on, including
499499
deleted ones. Requires admin permissions.
500+
enterprise_only Show all enterprise versions attached to this add-on.
501+
Requires either reviewer permissions or a user
502+
account listed as a developer of the add-on.
500503
==================== =====================================================
501504

502505
--------------
@@ -517,7 +520,7 @@ This endpoint allows you to fetch a single version belonging to a specific add-o
517520
:query string lang: Activate translations in the specific language for that query. (See :ref:`translated fields <api-overview-translations>`)
518521
:>json int id: The version id.
519522
:>json string approval_notes: Information for Mozilla reviewers, for when the add-on is reviewed. These notes are only visible to Mozilla, and this field is only present if the user has reviewer permissions, or is listed as a developer of the add-on.
520-
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted`` or ``listed``.
523+
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted``, ``listed``, or ``enterprise``.
521524
:>json object compatibility:
522525
Object detailing which :ref:`applications <addon-detail-application>` the version is compatible with.
523526
The exact min/max version numbers in the object correspond to the :ref:`supported versions<applications-version-list>`.
@@ -854,7 +857,7 @@ This endpoint is for uploading an addon file, to then be submitted to create a n
854857
.. _upload-create-request:
855858

856859
:form upload: The add-on file being uploaded.
857-
:form channel: The channel this version should be uploaded to, which determines its visibility on the site. It can be either ``unlisted`` or ``listed``.
860+
:form channel: The channel this version should be uploaded to, which determines its visibility on the site. It can be either ``unlisted``, ``listed``, or ``enterprise``.
858861
:reqheader Content-Type: multipart/form-data
859862

860863

@@ -901,7 +904,7 @@ This endpoint is for fetching a single previous upload by uuid.
901904
.. _upload-detail-object:
902905

903906
:>json string uuid: The upload id.
904-
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted`` or ``listed``.
907+
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted``, ``listed``, or ``enterprise``.
905908
:>json boolean processed: If the version has been processed by the validator.
906909
:>json boolean submitted: If this upload has been submitted as a new add-on or version already. An upload can only be submitted once.
907910
:>json string url: URL to check the status of this upload.

docs/topics/api/scanners.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ Rule results
223223
:query boolean was_signed: Only results whose file is signed.
224224
:query boolean addon_disabled_by_user: Only results whose add-on listing is
225225
invisible (``true``) or visible (``false``).
226-
:query string channel: Version channel, ``unlisted`` or ``listed``.
226+
:query string channel: Version channel. Can be either ``unlisted``, ``listed``, or ``enterprise``.
227227
:query string addon_status: Add-on status (e.g. ``public``, ``disabled``).
228228
:query string file_status: File status (e.g. ``public``, ``disabled``).
229229
:query string sort: Ordering field. One of ``id``, ``addon_adu``
@@ -237,7 +237,7 @@ Rule results
237237
:>json object results[].version: Minimal version representation (may be ``null``).
238238
:>json int results[].version.id: The version id.
239239
:>json string results[].version.version: The version number.
240-
:>json string results[].version.channel: The version channel (``listed`` or ``unlisted``).
240+
:>json string results[].version.channel: The version channel (``listed``, ``unlisted``, or ``enterprise``).
241241
:>json string results[].version.created: When the version was created.
242242
:>json object results[].version.addon: The add-on (``id``, ``guid``, ``average_daily_users``).
243243
:>json object results[].matches: The matched files and metadata, keyed by rule name.

src/olympia/addons/tests/test_views.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,6 +3165,54 @@ def test_unlisted_version_user_but_not_author(self):
31653165
response = self.client.get(self.url)
31663166
assert response.status_code == 403
31673167

3168+
def test_enterprise_version_reviewer(self):
3169+
user = UserProfile.objects.create(username='reviewer')
3170+
self.grant_permission(user, 'Addons:Review')
3171+
self.client.login_api(user)
3172+
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
3173+
response = self.client.get(self.url)
3174+
assert response.status_code == 403
3175+
3176+
def test_enterprise_version_enterprise_reviewer(self):
3177+
user = UserProfile.objects.create(username='reviewer')
3178+
self.grant_permission(user, 'Addons:ReviewUnlisted')
3179+
self.client.login_api(user)
3180+
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
3181+
self._test_url()
3182+
3183+
def test_enterprise_version_enterprise_viewer(self):
3184+
user = UserProfile.objects.create(username='reviewer')
3185+
self.grant_permission(user, 'ReviewerTools:ViewUnlisted')
3186+
self.client.login_api(user)
3187+
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
3188+
self._test_url()
3189+
3190+
def test_enterprise_version_author(self):
3191+
user = UserProfile.objects.create(username='author')
3192+
AddonUser.objects.create(user=user, addon=self.addon)
3193+
self.client.login_api(user)
3194+
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
3195+
self._test_url()
3196+
3197+
def test_enterprise_version_admin(self):
3198+
user = UserProfile.objects.create(username='admin')
3199+
self.grant_permission(user, '*:*')
3200+
self.client.login_api(user)
3201+
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
3202+
self._test_url()
3203+
3204+
def test_enterprise_version_anonymous(self):
3205+
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
3206+
response = self.client.get(self.url)
3207+
assert response.status_code == 401
3208+
3209+
def test_enterprise_version_user_but_not_author(self):
3210+
user = UserProfile.objects.create(username='simpleuser')
3211+
self.client.login_api(user)
3212+
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
3213+
response = self.client.get(self.url)
3214+
assert response.status_code == 403
3215+
31683216
def test_developer_version_serializer_used_for_authors(self):
31693217
self.version.update(source='src.zip')
31703218
# not logged in
@@ -5101,6 +5149,15 @@ def setUp(self):
51015149
channel=amo.CHANNEL_UNLISTED,
51025150
)
51035151

5152+
# This version is enterprise and should be hidden by default, only
5153+
# shown when requesting to see enterprise explicitly, with the
5154+
# right permissions.
5155+
self.enterprise_version = version_factory(
5156+
addon=self.addon,
5157+
version='5.0',
5158+
channel=amo.CHANNEL_ENTERPRISE,
5159+
)
5160+
51045161
self._set_tested_url(self.addon.pk)
51055162

51065163
def _test_url(self, **kwargs):
@@ -5146,6 +5203,16 @@ def _test_url_only_contains_old_version(self, **kwargs):
51465203
assert result_version['id'] == self.old_version.pk
51475204
assert result_version['version'] == self.old_version.version
51485205

5206+
def _test_url_only_contains_enterprise_version(self, **kwargs):
5207+
response = self.client.get(self.url, data=kwargs)
5208+
assert response.status_code == 200
5209+
result = json.loads(force_str(response.content))
5210+
assert result['results']
5211+
assert len(result['results']) == 1
5212+
result_version = result['results'][0]
5213+
assert result_version['id'] == self.enterprise_version.pk
5214+
assert result_version['version'] == self.enterprise_version.version
5215+
51495216
def _set_tested_url(self, param):
51505217
self.url = reverse_ns('addon-version-list', kwargs={'addon_pk': param})
51515218

@@ -5351,6 +5418,51 @@ def test_all_with_unlisted_when_no_unlisted_versions_viewer(self):
53515418
response = self.client.get(self.url, data={'filter': 'all_without_unlisted'})
53525419
assert response.status_code == 200
53535420

5421+
def test_enterprise_only_admin(self):
5422+
user = UserProfile.objects.create(username='admin')
5423+
self.grant_permission(user, '*:*')
5424+
self.client.login_api(user)
5425+
self._test_url_only_contains_enterprise_version(filter='enterprise_only')
5426+
5427+
def test_with_enterprise_unlisted_viewer(self):
5428+
user = UserProfile.objects.create(username='reviewer')
5429+
self.grant_permission(user, 'ReviewerTools:ViewUnlisted')
5430+
self.client.login_api(user)
5431+
self._test_url_only_contains_enterprise_version(filter='enterprise_only')
5432+
5433+
def test_with_enterprise_unlisted_author(self):
5434+
user = UserProfile.objects.create(username='author')
5435+
AddonUser.objects.create(user=user, addon=self.addon)
5436+
self.client.login_api(user)
5437+
self._test_url_only_contains_enterprise_version(filter='enterprise_only')
5438+
5439+
def test_enterprise_only_when_no_enterprise_versions(self):
5440+
user = UserProfile.objects.create(username='reviewer')
5441+
self.grant_permission(user, 'Addons:Review')
5442+
self.grant_permission(user, 'Addons:ReviewUnlisted')
5443+
self.client.login_api(user)
5444+
# delete the unlisted version so only the listed versions remain.
5445+
self.enterprise_version.delete()
5446+
5447+
# confirm that we have access to view unlisted versions.
5448+
response = self.client.get(self.url, data={'filter': 'enterprise_only'})
5449+
assert response.status_code == 200
5450+
result = json.loads(force_str(response.content))
5451+
assert len(result['results']) == 0
5452+
5453+
def test_enterprise_only_when_no_enterprise_versions_viewer(self):
5454+
user = UserProfile.objects.create(username='reviewer')
5455+
self.grant_permission(user, 'ReviewerTools:ViewUnlisted')
5456+
self.client.login_api(user)
5457+
# delete the unlisted version so only the listed versions remain.
5458+
self.enterprise_version.delete()
5459+
5460+
# confirm that we have access to view unlisted versions.
5461+
response = self.client.get(self.url, data={'filter': 'enterprise_only'})
5462+
assert response.status_code == 200
5463+
result = json.loads(force_str(response.content))
5464+
assert len(result['results']) == 0
5465+
53545466
def test_deleted_version_anonymous(self):
53555467
self.version.delete()
53565468
self._test_url_only_contains_old_version()

src/olympia/addons/views.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ def check_permissions(self, request):
563563
self.permission_classes = [
564564
GroupPermission(amo.permissions.ADDONS_VIEW_DELETED)
565565
]
566-
elif requested == 'all_with_unlisted':
566+
elif requested in ('all_with_unlisted', 'enterprise_only'):
567567
# To see unlisted versions, you need to be add-on author or
568568
# unlisted reviewer.
569569
self.permission_classes = [
@@ -602,7 +602,7 @@ def check_object_permissions(self, request, obj):
602602
).has_object_permission(request, self, obj):
603603
raise http.Http404
604604

605-
if obj.channel == amo.CHANNEL_UNLISTED:
605+
if obj.channel in (amo.CHANNEL_UNLISTED, amo.CHANNEL_ENTERPRISE):
606606
# If the instance is unlisted, only allow unlisted reviewers and
607607
# authors..
608608
self.permission_classes = [
@@ -625,11 +625,15 @@ def check_object_permissions(self, request, obj):
625625
def get_queryset(self):
626626
"""Return the right base queryset depending on the situation."""
627627
requested = self.request.GET.get('filter')
628-
valid_filters = (
628+
valid_filters = [
629629
'all_with_deleted',
630630
'all_with_unlisted',
631631
'all_without_unlisted',
632-
)
632+
]
633+
634+
if not is_gate_active(self.request, 'enterprise-channel-shim'):
635+
valid_filters.append('enterprise_only')
636+
633637
if requested is not None:
634638
if self.action != 'list':
635639
raise serializers.ValidationError(
@@ -650,11 +654,17 @@ def get_queryset(self):
650654
# `addon` property preloaded on each version we return.
651655
addon = self.get_addon_object()
652656
if requested == 'all_with_deleted' or self.action != 'list':
653-
queryset = addon.versions(manager='unfiltered_for_relations').all()
657+
queryset = addon.versions(manager='unfiltered_for_relations').filter(
658+
channel__in=(amo.CHANNEL_LISTED, amo.CHANNEL_UNLISTED)
659+
)
654660
elif requested == 'all_with_unlisted':
655-
queryset = addon.versions.all()
661+
queryset = addon.versions.filter(
662+
channel__in=(amo.CHANNEL_LISTED, amo.CHANNEL_UNLISTED)
663+
)
656664
elif requested == 'all_without_unlisted':
657665
queryset = addon.versions.filter(channel=amo.CHANNEL_LISTED)
666+
elif requested == 'enterprise_only':
667+
queryset = addon.versions.filter(channel=amo.CHANNEL_ENTERPRISE)
658668
else:
659669
queryset = addon.versions.filter(
660670
file__status=amo.STATUS_APPROVED, channel=amo.CHANNEL_LISTED

src/olympia/api/permissions.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,11 @@ def has_object_permission(self, request, view, obj):
227227
can_access_because_unlisted_reviewer = acl.is_unlisted_addons_reviewer(
228228
request.user
229229
) and (not self.disallow_unsafe or request.method in SAFE_METHODS)
230-
has_unlisted_or_no_listed = obj.has_unlisted_versions(
231-
include_deleted=True
232-
) or not obj.has_listed_versions(include_deleted=True)
230+
has_unlisted_or_no_listed = (
231+
obj.has_unlisted_versions(include_deleted=True)
232+
or obj.has_enterprise_versions(include_deleted=True)
233+
or not obj.has_listed_versions(include_deleted=True)
234+
)
233235

234236
return has_unlisted_or_no_listed and (
235237
can_access_because_unlisted_viewer or can_access_because_unlisted_reviewer

src/olympia/api/tests/test_permissions.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,11 +601,40 @@ def test_unlisted_viewer(self):
601601
self.request.method = 'GET'
602602
assert self.permission.has_object_permission(self.request, myview, obj)
603603

604+
def test_enterprise_reviewer(self):
605+
self.request.user = user_factory()
606+
self.grant_permission(self.request.user, 'Addons:ReviewUnlisted')
607+
obj = Mock(spec=[])
608+
obj.has_unlisted_versions = lambda include_deleted=False: False
609+
obj.has_enterprise_versions = lambda include_deleted=False: True
610+
611+
# Enterprise is restricted in similar manner to unlisted.
612+
assert self.permission.has_permission(self.request, myview)
613+
assert self.permission.has_object_permission(self.request, myview, obj)
614+
615+
def test_enterprise_viewer(self):
616+
self.request.user = user_factory()
617+
self.grant_permission(self.request.user, 'ReviewerTools:ViewUnlisted')
618+
obj = Mock(spec=[])
619+
obj.has_unlisted_versions = lambda include_deleted=False: False
620+
obj.has_enterprise_versions = lambda include_deleted=False: True
621+
622+
# Enterprise is restricted in similar manner to unlisted.
623+
assert self.permission.has_permission(self.request, myview)
624+
625+
# self.request is a POST, viewers should not have access to that.
626+
assert not self.permission.has_object_permission(self.request, myview, obj)
627+
628+
# GET requests should be allowed.
629+
self.request.method = 'GET'
630+
assert self.permission.has_object_permission(self.request, myview, obj)
631+
604632
def test_object_with_listed_versions_but_no_unlisted_versions(self):
605633
self.request.user = user_factory()
606634
self.grant_permission(self.request.user, 'Addons:ReviewUnlisted')
607635
obj = Mock(spec=[])
608636
obj.has_unlisted_versions = lambda include_deleted=False: False
637+
obj.has_enterprise_versions = lambda include_deleted=False: False
609638
obj.has_listed_versions = lambda include_deleted=False: True
610639

611640
assert self.permission.has_permission(self.request, myview)
@@ -616,6 +645,7 @@ def test_object_with_listed_versions_but_no_unlisted_versions_viewer(self):
616645
self.grant_permission(self.request.user, 'ReviewerTools:ViewUnlisted')
617646
obj = Mock(spec=[])
618647
obj.has_unlisted_versions = lambda include_deleted=False: False
648+
obj.has_enterprise_versions = lambda include_deleted=False: False
619649
obj.has_listed_versions = lambda include_deleted=False: True
620650

621651
assert self.permission.has_permission(self.request, myview)
@@ -626,6 +656,7 @@ def test_object_with_no_unlisted_versions_and_no_listed_versions(self):
626656
self.grant_permission(self.request.user, 'Addons:ReviewUnlisted')
627657
obj = Mock(spec=[])
628658
obj.has_unlisted_versions = lambda include_deleted=False: False
659+
obj.has_enterprise_versions = lambda include_deleted=False: False
629660
obj.has_listed_versions = lambda include_deleted=False: False
630661

631662
assert self.permission.has_permission(self.request, myview)
@@ -636,6 +667,7 @@ def test_object_with_no_unlisted_versions_and_no_listed_versions_viewer(self):
636667
self.grant_permission(self.request.user, 'ReviewerTools:ViewUnlisted')
637668
obj = Mock(spec=[])
638669
obj.has_unlisted_versions = lambda include_deleted=False: False
670+
obj.has_enterprise_versions = lambda include_deleted=False: False
639671
obj.has_listed_versions = lambda include_deleted=False: False
640672

641673
assert self.permission.has_permission(self.request, myview)

src/olympia/lib/settings_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,7 @@ def read_only_mode(env):
13211321
'categories-application',
13221322
'minimal-profile-has-all-fields-shim',
13231323
'promoted-groups-shim',
1324+
'enterprise-channel-shim',
13241325
),
13251326
'v4': (
13261327
'l10n_flat_input_output',
@@ -1340,6 +1341,7 @@ def read_only_mode(env):
13401341
'block-versions-list-shim',
13411342
'promoted-groups-shim',
13421343
'minimal-profile-has-all-fields-shim',
1344+
'enterprise-channel-shim',
13431345
),
13441346
'v5': (
13451347
'addons-search-_score-field',

src/olympia/signing/views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,9 @@ def handle_upload(self, request, addon, version_string, guid=None):
199199
created = False
200200
channel_param = request.POST.get('channel')
201201
channel = amo.CHANNEL_CHOICES_LOOKUP.get(channel_param)
202-
if not channel:
202+
if (
203+
not channel or channel == amo.CHANNEL_ENTERPRISE # api/v5+ only
204+
):
203205
last_version = addon.find_latest_version(None, exclude=())
204206
if last_version:
205207
channel = last_version.channel

0 commit comments

Comments
 (0)