Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/topics/api/addons.rst
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,9 @@ This endpoint allows you to list all versions belonging to a specific add-on.
a user account listed as a developer of the add-on.
all_with_deleted Show all versions attached to this add-on, including
deleted ones. Requires admin permissions.
enterprise_only Show all enterprise versions attached to this add-on.
Requires either reviewer permissions or a user
account listed as a developer of the add-on.
==================== =====================================================

--------------
Expand All @@ -517,7 +520,7 @@ This endpoint allows you to fetch a single version belonging to a specific add-o
:query string lang: Activate translations in the specific language for that query. (See :ref:`translated fields <api-overview-translations>`)
:>json int id: The version id.
:>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.
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted`` or ``listed``.
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted``, ``listed``, or ``enterprise``.
:>json object compatibility:
Object detailing which :ref:`applications <addon-detail-application>` the version is compatible with.
The exact min/max version numbers in the object correspond to the :ref:`supported versions<applications-version-list>`.
Expand Down Expand Up @@ -854,7 +857,7 @@ This endpoint is for uploading an addon file, to then be submitted to create a n
.. _upload-create-request:

:form upload: The add-on file being uploaded.
:form channel: The channel this version should be uploaded to, which determines its visibility on the site. It can be either ``unlisted`` or ``listed``.
: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``.
:reqheader Content-Type: multipart/form-data


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

:>json string uuid: The upload id.
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted`` or ``listed``.
:>json string channel: The version channel, which determines its visibility on the site. Can be either ``unlisted``, ``listed``, or ``enterprise``.
:>json boolean processed: If the version has been processed by the validator.
:>json boolean submitted: If this upload has been submitted as a new add-on or version already. An upload can only be submitted once.
:>json string url: URL to check the status of this upload.
Expand Down
24 changes: 24 additions & 0 deletions src/olympia/activity/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ def test_get_not_listed_author(self):
response = self.client.get(self.url)
assert response.status_code == 200

def test_get_enterprise_simple_reviewer(self):
self.change_channel_for_addon(self.addon, amo.CHANNEL_ENTERPRISE)
self._login_reviewer()
response = self.client.get(self.url)
assert response.status_code == 403

def test_get_enterprise_specific_reviewer(self):
self.change_channel_for_addon(self.addon, amo.CHANNEL_ENTERPRISE)
self._login_reviewer(permission=amo.permissions.ADDONS_REVIEW_UNLISTED)
response = self.client.get(self.url)
assert response.status_code == 200

def test_get_enterprise_unlisted_viewer(self):
self.change_channel_for_addon(self.addon, amo.CHANNEL_ENTERPRISE)
self._login_reviewer(permission=amo.permissions.REVIEWER_TOOLS_UNLISTED_VIEW)
response = self.client.get(self.url)
assert response.status_code == 200

def test_get_enterprise_author(self):
self.change_channel_for_addon(self.addon, amo.CHANNEL_ENTERPRISE)
self._login_developer()
response = self.client.get(self.url)
assert response.status_code == 200

def test_get_deleted(self):
self.addon.delete()
self._login_developer()
Expand Down
5 changes: 4 additions & 1 deletion src/olympia/addons/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1908,7 +1908,10 @@ def validate(self, data):
gettext('Rollback is only available for version %s in this channel.')
% first.version
)
elif existing.channel == amo.CHANNEL_UNLISTED and existing not in available:
elif (
existing.channel in (amo.CHANNEL_UNLISTED, amo.CHANNEL_ENTERPRISE)
and existing not in available
):
raise exceptions.ValidationError(
gettext(
'Only approved versions can be rolled back, except the most recent '
Expand Down
209 changes: 202 additions & 7 deletions src/olympia/addons/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
ApplicationsVersions,
AppVersion,
License,
VersionCreateError,
VersionPreview,
VersionProvenance,
VersionReviewerFlags,
Expand Down Expand Up @@ -390,6 +391,32 @@ def test_get_not_listed_author(self):
response = self.client.get(self.url)
assert response.status_code == 200

def test_get_enterprise_no_rights(self):
user = UserProfile.objects.create(username='user')
self.change_channel_for_addon(self.addon, amo.CHANNEL_ENTERPRISE)
self.client.login_api(user)
response = self.client.get(self.url)
assert response.status_code == 403
data = json.loads(force_str(response.content))
assert data['detail'] == ('You do not have permission to perform this action.')

def test_get_enterprise_addons_api_view_unlisted(self):
user = UserProfile.objects.create(username='user')
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW)
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW_UNLISTED)
self.change_channel_for_addon(self.addon, amo.CHANNEL_ENTERPRISE)
self.client.login_api(user)
response = self.client.get(self.url)
assert response.status_code == 200

def test_get_enterprise_author(self):
user = UserProfile.objects.create(username='author')
AddonUser.objects.create(user=user, addon=self.addon)
self.change_channel_for_addon(self.addon, amo.CHANNEL_ENTERPRISE)
self.client.login_api(user)
response = self.client.get(self.url)
assert response.status_code == 200

def test_get_deleted(self):
self.addon.delete()
response = self.client.get(self.url)
Expand Down Expand Up @@ -3127,6 +3154,43 @@ def test_unlisted_version_user_but_not_author(self):
response = self.client.get(self.url)
assert response.status_code == 403

def test_enterprise_version_no_unlisted_permission(self):
user = UserProfile.objects.create(username='user')
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW)
self.client.login_api(user)
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
response = self.client.get(self.url)
assert response.status_code == 403

def test_enterprise_version_addons_api_view_unlisted(self):
user = UserProfile.objects.create(username='reviewer')
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW)
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW_UNLISTED)
self.client.login_api(user)
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
self._test_url()

def test_enterprise_version_not_author(self):
user = UserProfile.objects.create(username='user')
self.client.login_api(user)
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
response = self.client.get(self.url)
assert response.status_code == 403

def test_enterprise_version_author(self):
user = UserProfile.objects.create(username='author')
AddonUser.objects.create(user=user, addon=self.addon)
self.client.login_api(user)
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
self._test_url()

def test_enterprise_version_admin(self):
user = UserProfile.objects.create(username='admin')
self.grant_permission(user, amo.permissions.SUPERPOWERS)
self.client.login_api(user)
self.version.update(channel=amo.CHANNEL_ENTERPRISE)
self._test_url()

def test_developer_version_serializer_used_for_authors(self):
self.version.update(source='src.zip')
# not logged in
Expand Down Expand Up @@ -3689,6 +3753,46 @@ def test_basic_unlisted(self):
assert provenance.source == amo.UPLOAD_SOURCE_ADDON_API
assert provenance.client_info == 'web-ext/12.34'

@override_switch('enterprise-channel', active=True)
def test_basic_enterprise(self):
self.upload.update(channel=amo.CHANNEL_ENTERPRISE)
response = self.client.post(
self.url,
data=self.minimal_data,
HTTP_USER_AGENT='web-ext/12.34',
)
assert response.status_code == 201, response.content
data = response.data
assert data['license'] is None
assert data['compatibility'] == {
'firefox': {'max': '*', 'min': amo.DEFAULT_WEBEXT_MIN_VERSION},
}
self.addon.reload()
assert self.addon.versions.count() == 2
version = self.addon.find_latest_version(channel=None)
request = APIRequestFactory().get('/')
request.version = 'v5'
request.user = self.user
assert data == DeveloperVersionSerializer(
context={'request': request}
).to_representation(version)
assert version.channel == amo.CHANNEL_ENTERPRISE
self.statsd_incr_mock.assert_any_call('addons.submission.version.enterprise')
self.statsd_incr_mock.assert_any_call('addons.submission.webext_version.12_34')
provenance = VersionProvenance.objects.get()
assert provenance.version == version
assert provenance.source == amo.UPLOAD_SOURCE_ADDON_API
assert provenance.client_info == 'web-ext/12.34'

def test_enterprise_switch_off(self):
self.upload.update(channel=amo.CHANNEL_ENTERPRISE)
with self.assertRaises(VersionCreateError):
self.client.post(
self.url,
data=self.minimal_data,
)
assert self.addon.reload().versions.count() == 1

@patch('olympia.addons.views.log')
def test_does_not_log_without_source(self, log_mock):
response = self.client.post(
Expand Down Expand Up @@ -5047,12 +5151,12 @@ def setUp(self):
guid=generate_addon_guid(), name='My Addôn', slug='my-addon'
)
self.old_version = self.addon.current_version
self.old_version.update(created=self.days_ago(2))
self.old_version.update(created=self.days_ago(3))

# Don't use addon.current_version, changing its state as we do in
# the tests might render the add-on itself inaccessible.
self.version = version_factory(addon=self.addon, version='1.0.1')
self.version.update(created=self.days_ago(1))
self.version.update(created=self.days_ago(2))

# This version is unlisted and should be hidden by default, only
# shown when requesting to see unlisted stuff explicitly, with the
Expand All @@ -5062,6 +5166,16 @@ def setUp(self):
version=amo.DEFAULT_WEBEXT_MIN_VERSION,
channel=amo.CHANNEL_UNLISTED,
)
self.unlisted_version.update(created=self.days_ago(1))

# This version is enterprise and should be hidden by default, only
# shown when requesting to see enterprise explicitly, with the
# right permissions.
self.enterprise_version = version_factory(
addon=self.addon,
version='5.0',
channel=amo.CHANNEL_ENTERPRISE,
)

self._set_tested_url(self.addon.pk)

Expand All @@ -5087,14 +5201,17 @@ def _test_url_contains_all(self, **kwargs):
assert response.status_code == 200
result = json.loads(force_str(response.content))
assert result['results']
assert len(result['results']) == 3
assert len(result['results']) == 4
result_version = result['results'][0]
assert result_version['id'] == self.enterprise_version.pk
assert result_version['version'] == self.enterprise_version.version
result_version = result['results'][1]
assert result_version['id'] == self.unlisted_version.pk
assert result_version['version'] == self.unlisted_version.version
result_version = result['results'][1]
result_version = result['results'][2]
assert result_version['id'] == self.version.pk
assert result_version['version'] == self.version.version
result_version = result['results'][2]
result_version = result['results'][3]
assert result_version['id'] == self.old_version.pk
assert result_version['version'] == self.old_version.version

Expand All @@ -5108,6 +5225,17 @@ def _test_url_only_contains_old_version(self, **kwargs):
assert result_version['id'] == self.old_version.pk
assert result_version['version'] == self.old_version.version

def _test_url_only_contains_enterprise_version(self, **kwargs):
response = self.client.get(self.url, data=kwargs)
assert response.status_code == 200
result = json.loads(force_str(response.content))
assert result['results']
assert len(result['results']) == 1
result_version = result['results'][0]
assert result_version['id'] == self.enterprise_version.pk
assert result_version['version'] == self.enterprise_version.version
assert result_version['channel'] == 'enterprise'

def _set_tested_url(self, param):
self.url = reverse_ns('addon-version-list', kwargs={'addon_pk': param})

Expand Down Expand Up @@ -5270,8 +5398,9 @@ def test_all_with_unlisted_when_no_unlisted_versions(self):
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW)
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW_UNLISTED)
self.client.login_api(user)
# delete the unlisted version so only the listed versions remain.
# delete the unlisted versions so only the listed versions remain.
self.unlisted_version.delete()
self.enterprise_version.delete()

# confirm that we have access to view unlisted versions.
response = self.client.get(self.url, data={'filter': 'all_with_unlisted'})
Expand All @@ -5287,6 +5416,71 @@ def test_all_with_unlisted_when_no_unlisted_versions(self):
response = self.client.get(self.url, data={'filter': 'all_without_unlisted'})
assert response.status_code == 200

def test_with_enterprise_addons_api_view_unlisted(self):
user = UserProfile.objects.create(username='reviewer')
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW)
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW_UNLISTED)
self.client.login_api(user)
self._test_url_only_contains_enterprise_version(filter='enterprise_only')

def test_enterprise_only_when_no_enterprise_versions(self):
user = UserProfile.objects.create(username='reviewer')
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW)
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW_UNLISTED)
self.client.login_api(user)
# delete the unlisted version so only the listed versions remain.
self.enterprise_version.delete()

# confirm that we have access to view unlisted versions.
response = self.client.get(self.url, data={'filter': 'enterprise_only'})
assert response.status_code == 200
result = json.loads(force_str(response.content))
assert len(result['results']) == 0

def test_enterprise_only_logout(self):
response = self.client.get(self.url, data={'filter': 'enterprise_only'})
assert response.status_code == 401

def test_enterprise_only_not_author(self):
user = UserProfile.objects.create(username='user')
self.client.login_api(user)
response = self.client.get(self.url, data={'filter': 'enterprise_only'})
assert response.status_code == 403

def test_enterprise_only_author(self):
user = UserProfile.objects.create(username='author')
AddonUser.objects.create(user=user, addon=self.addon)
user.update(read_dev_agreement=None)
self.client.login_api(user)
self._test_url_only_contains_enterprise_version(filter='enterprise_only')

def test_enterprise_only_with_superpowers(self):
user = UserProfile.objects.create(username='user')
self.grant_permission(user, amo.permissions.SUPERPOWERS)
self.client.login_api(user)
self._test_url_only_contains_enterprise_version(filter='enterprise_only')

def test_enterprise_only_not_available_in_old_api_versions(self):
current_api_version = settings.REST_FRAMEWORK['DEFAULT_VERSION']
assert (
'enterprise-channel-shim' not in settings.DRF_API_GATES[current_api_version]
)
user = UserProfile.objects.create(username='admin')
self.grant_permission(user, amo.permissions.SUPERPOWERS)
self.client.login_api(user)

for api_version in ('v3', 'v4'):
assert 'enterprise-channel-shim' in settings.DRF_API_GATES[api_version]
url = reverse_ns(
'addon-version-list',
api_version=api_version,
kwargs={'addon_pk': self.addon.pk},
)
response = self.client.get(url, data={'filter': 'enterprise_only'})
assert response.status_code == 400
data = json.loads(force_str(response.content))
assert data == ['Invalid "filter" parameter specified.']

def test_deleted_version_anonymous(self):
self.version.delete()
self._test_url_only_contains_old_version()
Expand Down Expand Up @@ -5323,9 +5517,10 @@ def test_all_without_unlisted_when_no_listed_versions(self):
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW)
self.grant_permission(user, amo.permissions.ADDONS_API_VIEW_UNLISTED)
self.client.login_api(user)
# delete the listed versions so only the unlisted version remains.
# delete the other versions so only the unlisted version remains.
self.version.delete()
self.old_version.delete()
self.enterprise_version.delete()

# confirm that we have access to view unlisted versions.
response = self.client.get(self.url, data={'filter': 'all_with_unlisted'})
Expand Down
Loading
Loading