diff --git a/extensions/models.py b/extensions/models.py index 8c604325..1880982c 100644 --- a/extensions/models.py +++ b/extensions/models.py @@ -492,7 +492,11 @@ class VersionManager(models.Manager): def update_or_create(self, *args, **kwargs): # Stash the ManyToMany to be created after the Version has a valid ID already licenses = kwargs.pop('licenses', []) - permissions = kwargs.pop('permissions', []) + permissions = kwargs.pop('permissions', {}) + # FIXME: ideally this dict->list transformation should be a caller's responsibility + # but we plan to change the model soon, so getting a dict as an input here makes sense + if permissions: + permissions = list(permissions.keys()) platforms = kwargs.pop('platforms', []) tags = kwargs.pop('tags', []) diff --git a/extensions/tests/files/addon-with-permissions.zip b/extensions/tests/files/addon-with-permissions.zip new file mode 100644 index 00000000..45939564 Binary files /dev/null and b/extensions/tests/files/addon-with-permissions.zip differ diff --git a/extensions/tests/files/invalid-missing-wheels.zip b/extensions/tests/files/invalid-missing-wheels.zip index 76c12484..93aae3b3 100644 Binary files a/extensions/tests/files/invalid-missing-wheels.zip and b/extensions/tests/files/invalid-missing-wheels.zip differ diff --git a/extensions/tests/test_manifest.py b/extensions/tests/test_manifest.py index 431a1e3d..94480a3c 100644 --- a/extensions/tests/test_manifest.py +++ b/extensions/tests/test_manifest.py @@ -523,19 +523,35 @@ class ValidateManifestFields(TestCase): **self.optional_fields, } - data['permissions'] = ['files'] + data['permissions'] = {'files': 'test files'} ManifestValidator(data) data.pop('permissions') ManifestValidator(data) - data['permissions'] = ['non-supported-permission'] + # punctuation + data['permissions'] = {'files': 'lalala.'} with self.assertRaises(ValidationError) as e: ManifestValidator(data) - message_begin = "Manifest value error: permissions expects a list of" + # empty reason + data['permissions'] = {'files': ''} + with self.assertRaises(ValidationError) as e: + ManifestValidator(data) + + # too long reason + data['permissions'] = { + 'files': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + } + with self.assertRaises(ValidationError) as e: + ManifestValidator(data) + + data['permissions'] = {'non-supported-permission': 'lalala'} + with self.assertRaises(ValidationError) as e: + ManifestValidator(data) + message_begin = "Manifest value error: permissions expects key/value pairs of" self.assertIn(message_begin, e.exception.messages[0]) - self.assertIn('[\'files\', \'network\']', e.exception.messages[0]) + self.assertIn('files, network', e.exception.messages[0]) # Check the permissions that will always be around. message_end = e.exception.messages[0][len(message_begin) :] @@ -546,13 +562,13 @@ class ValidateManifestFields(TestCase): for permission in VersionPermission.objects.all(): self.assertIn(permission.slug, message_end) - data['permissions'] = [] + data['permissions'] = {} with self.assertRaises(ValidationError) as e: ManifestValidator(data) self.assertEqual(1, len(e.exception.messages)) # Make sure permissions are only defined for add-ons, but fail for themes. - data['permissions'] = ['files'] + data['permissions'] = {'files': 'test files'} data['type'] = EXTENSION_TYPE_SLUGS_SINGULAR[EXTENSION_TYPE_CHOICES.THEME] data['tags'] = [] with self.assertRaises(ValidationError) as e: @@ -693,7 +709,7 @@ class VersionPermissionsTest(CreateFileTest): def test_version_permission_register(self): """Make sure permissions are saved""" - permissions = ['network'] + permissions = {'network': 'talk to server'} latest_version = self._test_version_permission(permissions) self.assertEqual(latest_version.permissions.count(), 1) self.assertEqual(latest_version.permissions.first().slug, 'network') diff --git a/extensions/tests/test_submit.py b/extensions/tests/test_submit.py index 3cc6cbe1..300b63da 100644 --- a/extensions/tests/test_submit.py +++ b/extensions/tests/test_submit.py @@ -63,6 +63,22 @@ EXPECTED_EXTENSION_DATA = { 'version_str': '1.0.8', 'slug': 'amaranth', }, + 'addon-with-permissions.zip': { + 'metadata': { + 'tagline': 'Some add-on tag line', + 'name': 'Some Add-on', + 'id': 'some_addon', + 'version': '0.1.0', + 'blender_version_min': '4.2.0', + 'type': 'add-on', + 'permissions': {'files': 'reading files', 'network': 'talking to server'}, + }, + 'file_hash': 'sha256:cce5f6268cb096cf8b070b17cab78ad2bc10f2ff85c625ef4a2b6db7c01a29ad', + 'size_bytes': 748, + 'tags': [], + 'version_str': '0.1.0', + 'slug': 'some-addon', + }, } EXPECTED_VALIDATION_ERRORS = { 'empty.txt': {'source': ['Only .zip files are accepted.']}, @@ -147,6 +163,7 @@ class SubmitFileTest(TestCase): self.assertEqual(file.hash, file_hash) self.assertEqual(file.get_type_display(), 'Add-on') self.assertEqual(file.metadata['version'], version_str) + self.assertEqual(file.metadata.get('permissions'), other_metadata.get('permissions')) def test_not_allowed_anonymous(self): with open(TEST_FILES_DIR / 'edit_breakdown-0.1.0.zip', 'rb') as fp: diff --git a/extensions/views/api.py b/extensions/views/api.py index 1f410ca4..0fe10298 100644 --- a/extensions/views/api.py +++ b/extensions/views/api.py @@ -102,7 +102,10 @@ class ListedExtensionsSerializer(serializers.ModelSerializer): # avoid triggering additional db queries, reuse the prefetched queryset 'maintainer': instance.team and instance.team.name or str(instance.authors.all()[0]), 'license': [license_iter.slug for license_iter in matching_version.licenses.all()], - 'permissions': [permission.slug for permission in matching_version.permissions.all()], + 'permissions': { + permission.slug: permission.help + for permission in matching_version.permissions.all() + }, 'platforms': [platform.slug for platform in matching_version.platforms.all()], # TODO: handle copyright 'tags': [str(tag) for tag in matching_version.tags.all()], diff --git a/files/validators.py b/files/validators.py index 63fb1956..f3923f1a 100644 --- a/files/validators.py +++ b/files/validators.py @@ -321,7 +321,10 @@ class TypeValidator: class PermissionsValidator: - example = ['files', 'network'] + example = { + 'files': "Import/export FBX from/to disk", + 'network': "Need to sync motion-capture data to server", + } @classmethod def validate(cls, *, name: str, value: str, manifest: dict) -> str: @@ -342,22 +345,47 @@ class PermissionsValidator: if not is_bpy: return - if (type(value) != list) or (not value): + if not isinstance(value, dict) or not value: is_error = True else: - for permission in value: + for permission, reason in value.items(): try: VersionPermission.get_by_slug(permission) except VersionPermission.DoesNotExist: is_error = True logger.info(f'Permission unavailable: {permission}') + if not isinstance(reason, str): + return mark_safe( + 'Manifest value error: permissions reasons must be strings.' + ) + + if not reason: + return mark_safe( + f'Manifest value error: permissions {permission} reason ' + 'is empty.' + ) + + if reason[-1] in {'.', '!', '?'}: + return mark_safe( + 'Manifest value error: permissions {permission} reason ' + 'cannot end with any punctuation (.!?).' + ) + + max_length = 64 + if len(reason) > max_length: + return mark_safe( + f'Manifest value error: permissions {permission} reason is ' + 'too long ({len(value)}), ' + f'max-length: {max_length} characters' + ) if not is_error: return error_message = ( - f'Manifest value error: permissions expects a list of ' - f'supported permissions. e.g.: {cls.example}. The supported permissions are: ' + 'Manifest value error: permissions expects key/value pairs of ' + 'supported permissions with the reasons why they are required. ' + 'The supported permissions are: ' ) for permission_ob in VersionPermission.objects.all():