Skip to content

[change] Improve endpoints to download firmware images #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

youhaveme9
Copy link
Member

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #69

Please open a new issue if there isn't an existing issue yet.

Copy link
Member

@pandafy pandafy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks on the correct track. Check my suggestion below.

@@ -970,9 +985,11 @@ def test_firmware_download(self):
url = reverse('upgrader:api_firmware_download', args=[image.build.pk, image.pk])
with self.subTest("Test as operator"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add a sub-test to also test as an administrator.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are testing it for admin too below

)

# Create a custom permission group without view permission
from django.contrib.auth.models import Group
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason why this import isn't at the top of the file?

Load the Group model using the swapper module

import swapper 
Group = swapper.load_model('openwisp_users', 'Group')

Comment on lines 28 to 33
if not user.is_superuser:
perm_codename = get_permission_codename('view', self.model._meta)
view_perm = f'{self.model._meta.app_label}.{perm_codename}'
has_view_perm = user.has_perm(view_perm, self.object)
if not has_view_perm:
return False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider using the PermissionRequiredMixin instead of performing these checks manually?

Comment on lines 67 to 72
def to_representation(self, instance):
ret = super().to_representation(instance)
# Replace 'file' with the URL from file_url
if 'file_url' in ret:
ret['file'] = ret.pop('file_url')
return ret
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are already overriding the value of field value in the to_representation, then why do we need to add another field file_url to the serializer? We can directly call the get_file_url

@pandafy pandafy moved this from To do (general) to In progress in OpenWISP Contributor's Board Apr 28, 2025
Copy link
Member

@pandafy pandafy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced with the changes to the tests. Can you clarify my doubts?

@@ -45,10 +46,26 @@ class Meta:


class FirmwareImageSerializer(BaseSerializer):
file = serializers.FileField(required=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why defining this field is requried? I thought, it would be added automatically by ModelSerializer.

Comment on lines 65 to 66
elif hasattr(instance, 'file'):
ret['file'] = instance.file.url
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the default behaviour? If we leave the 'file' field unchanged in the code, then it will contain the file.url.

I tested this locally with the following code:

In [1]: from openwisp_firmware_upgrader.api.serializers import FirmwareImage, FirmwareImageSerializer
   ...: 
   ...: fw_image = FirmwareImage.objects.first()
   ...: fw_image.file.url
   ...: 
   ...: serializer = FirmwareImageSerializer(instance=fw_image)

In [2]: serializer.data
Out[2]: {'id': '144d9284-6b80-471b-a2dd-52cbc3c18565', 'file': '/firmware/503ac721-94fd-469f-85a7-977330f50499/openwrt-23.05.0-ramips-mt7621-yuncore_ax820-squashfs-sysupgrade.bin', 'created': '2025-04-28T11:15:07.867479+02:00', 'modified': '2025-04-28T11:15:07.867479+02:00', 'type': 'ramips-mt7621-yuncore_ax820-squashfs-sysupgrade.bin', 'build': UUID('503ac721-94fd-469f-85a7-977330f50499')}

I think, this change makes the serializer inconsistent. We should try to return the URL to the firmware download API even if request is not present in the serializer. In order to remain consistent with DRF, we can return the relative path to the API firmware download view if there's no request object.

)
has_view_perm = user.has_perm(perm, self.object)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to check this here as the PermissionRequiredMixin should handle this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also skip accessing the object here. We don't enforce object level permission checks in OpenWISP.

Comment on lines 43 to 49
def handle_no_permission(self):
"""
Return empty response for unauthorized API requests
"""
if 'api' in self.request.path:
return HttpResponse(status=403)
return super().handle_no_permission()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I commented this code, and none of the tests fails. This is bad.

Is this code relevant? Won't raise_exception = True cover this scenario?

url = reverse('upgrader:api_firmware_download', args=[image.build.pk, image.pk])
with self.subTest(url=url):
with self.assertNumQueries(1):
r = client.get(url)
self.assertEqual(r.status_code, 401)
self.assertEqual(r.status_code, 403)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. We should fix the view so it returns 401 instead of 403 for unauthenticated users.

@@ -68,9 +74,41 @@ def test_staff_operator_with_same_organization(self):
user=staff_user, organization=self.test_org, is_admin=True
)
self.client.force_login(staff_user)
self._download_firmware_assert_status(200)
self._download_firmware_assert_status(403)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change this? Aren't operators allowed to view the FirmwareImage?

operators_read_only_admins_manage = [
'build',
'devicefirmware',
'firmwareimage',
'batchupgradeoperation',
'upgradeoperation',
]


def test_superuser(self):
user = self._get_admin()
self.client.force_login(user)
self._download_firmware_assert_status(200)

def test_view_permission_check(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this test is correct? I believe the code comments are outdated.

A user being Organization Admin and a user having Administrator group are two different things. Is this clear?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes clear now

Comment on lines 33 to 42
# Get object first since it's needed for organization check
self.object = self.get_object()

# Superusers can always access
if user.is_superuser:
return True

return user.is_staff and user.is_manager(
self.object.build.category.organization
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the logic stays same as before?

Comment on lines 90 to 123
# Add view permission first
content_type = ContentType.objects.get_for_model(FirmwareImage)
perm_codename = get_permission_codename('view', FirmwareImage._meta)
view_perm = Permission.objects.get(
content_type=content_type, codename=perm_codename
)
staff_user.user_permissions.add(view_perm)

self._create_org_user(user=staff_user, organization=org, is_admin=True)
self._download_firmware_assert_status(200)

# Remove org manager status
org_user = staff_user.openwisp_users_organization.get(
organization_users__organization=org
)
org_user.is_admin = False
org_user.save()

# Remove staff status
staff_user.is_staff = False
staff_user.save()

# Restore org manager status
org_user.is_admin = True
org_user.save()

# No access (missing staff status)
self._download_firmware_assert_status(403)

# Remove view permission
staff_user.user_permissions.remove(view_perm)

# No access (missing view permission)
self._download_firmware_assert_status(403)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, can you refactor the code to use subTest? It helps to organize code for testing different scenarios. You can look around the code for reference.

https://github.com/search?q=repo%3Aopenwisp%2Fopenwisp-firmware-upgrader+subTest&type=code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In progress
Development

Successfully merging this pull request may close these issues.

[change] Improve endpoints to download firmware images
2 participants